Am new to unit testing, and I have been struggling with the right approach to test private functions. I have done my research, and am going with testing through the public interface.
My problem is the following, if I have a private function used by two public functions, I encounter test case duplicates, which I don't know how to overcome. Extracting the private function into another class doesn't seem appropriate in the case am facing, as the function is sufficient only for the class am using it from.
Example:
// This function is called only once, once the view is loaded
override func handleViewDidLoad() {
super.handleViewDidLoad()
loadData()
// I have extra setup code in here
}
// This function is called when user double taps on the screen
override func handleReloadData() {
super.handleReloadData()
loadData()
}
private func loadData() {
delegate.showLoading()
fetchData { [weak self] in
self?.delegate?.hideLoading()
}
}
// I have another function that uses this one. But it's not included in the sample
private func fetchData(then completion: @escaping (() -> Void)) {
service.start() { [weak self] (_, error) in
if error == nil {
if self?.shouldTrackScreen == false {
self?.shouldTrackScreen = true
self?.delegate?.trackScreen()
}
}
completion()
}
}
So when I think about handleViewDidLoad
function's test cases. I will have the following:
- Check that
showLoading
is called. - Check that
hideLoading
is called in case of service success. - Check that
hideLoading
is called in case of service failure. - Check that
service.start()
is called.
Same applies to handleReloadData
test cases. This way I will end up with a duplicate of each test case of the above.
And if I have another function that uses fetchData
function, I will also have to repeat the 4th test case.
Is there a way to overcome those duplicates?
Edit
Sample test cases for clarification
1- handleViewDidLoad
tests
func testHandleViewDidLoadCallsShowLoading() {
// Given
stubService(forAction: actionType)
// When
sut.handleViewDidLoad()
// Then
verify(mockDelegate, times(1)).showLoading()
}
func testHandleViewDidLoadCallsHideLoadingWhenServiceSucceeds() {
// Given
stubService(forAction: actionType)
// When
sut.handleViewDidLoad()
// Then
verify(mockDelegate, times(1)).hideLoading()
}
func testHandleViewDidLoadCallsHideLoadingWhenServiceFails() {
// Given
stubService(forAction: actionType, error: VFAppError())
// When
sut.handleViewDidLoad()
// Then
verify(mockDelegate, times(1)).hideLoading()
}
2- handleReloadData
tests
func testHandleReloadDataCallsShowLoading() {
// Given
stubService(forAction: actionType)
// When
sut.handleReloadData()
// Then
verify(mockDelegate, times(1)).showLoading()
}
func testHandleReloadDataCallsHideLoadingWhenServiceSucceeds() {
// Given
stubService(forAction: actionType)
// When
sut.handleReloadData()
// Then
verify(mockDelegate, times(1)).hideLoading()
}
func testHandleReloadDataCallsHideLoadingWhenServiceFails() {
// Given
stubService(forAction: actionType, error: Error())
// When
sut.handleReloadData()
// Then
verify(mockDelegate, times(1)).hideLoading()
}
and in the setup
method that gets run before every test case I have this
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
sut = Presenter(viewController: mockDelegate, service: mockService)
stubDelegate()
}
handleViewDidLoad
versushandleReloadData
, so even if the assertions about the end state are the same, the preconditions are different.handleViewDidLoad
was called andhideLoading
was not triggered, this leads to a loading view that blocks the visibility of screen's data, even though the data are loaded behind it.