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()
}
2 Answers 2
If I have a private function used by two public functions, I encounter test case duplicates
No you don’t.
I know people say the way to test a private function is through the public function that uses it. This is correct but it’s still the wrong way to think about it.
You don’t test "private functions". You test ALL the code behind the public interface of the unit, the abstraction, the system under test (which might be a public function).
The fancy way to say "ALL the code" is code coverage. If what you’re testing uses a private function then your test better exercise it. Not because it’s a function. Because it’s part of what you’re testing.
If you’re worried that you’re exercising the same code twice stop it. The tests shouldn’t even know that. Unless this private function slows the system to a crawl you’re just micro optimizing. Don’t expect anyone to thank you for solving non problems.
It makes about as much sense to mock out the private function as it would to mock out a public library call like Math.abs()
. So long as it’s timely, deterministic, and free of weird dependencies and side effects just think of it as part of the public function.
A test should never know about how a public function works. Just that it works. That way you decide if you use a private function or not. The test doesn’t get to have an opinion.
-
1This got lots of upvotes, but misses completely the point for this specific example (maybe because of the red herring of "testing private functions" in the title). When reading the code thoroughly, one sees the OP wants to check some externally visible behaviour (calls of some delegates) which is expected to be the same for two public functions. Where these calls happen inside and that they are implemented in some private method is pretty irrelevant to the question.Doc Brown– Doc Brown09/29/2020 07:37:01Commented Sep 29, 2020 at 7:37
-
Maybe, because testing code can not address a design flaw, it just makes it obviousLaiv– Laiv09/29/2020 12:57:12Commented Sep 29, 2020 at 12:57
-
1@Laiv: not everything we don't understand immediately in full is a design flaw.Doc Brown– Doc Brown09/29/2020 13:10:19Commented Sep 29, 2020 at 13:10
-
-
@Laiv - Regarding a potential design flaw: one way to test code is to use a kind of test double known as a Spy (these verify if one or more methods were called, how many times, etc.); these are sometimes useful but shouldn't be used extensively throughout the test suite because they tightly couple the tests and the SUT. Assuming the current design is OK, the OP's tests are pretty much doing the same thing, it's just that the OP couldn't see how to factor out the bit that varies, perhaps because of the way this
verify
construct of the mocking framework was designed.Filip Milovanović– Filip Milovanović09/30/2020 07:57:45Commented Sep 30, 2020 at 7:57
Here is a dead simple solution:
If you have several test cases which validate the same conditions or the same behaviour for different entry functions of the same object, refactor the duplicate assertion code into a common function.
In the afterwards given example, the member function of the sut which is called has to be a parameter of the extracted function. I don't know Swift, but I guess it has the necessary functional tools, so take this as pseudocode:
func testMySutFuncionCallsShowLoading( mySutFunction: ()->() ) {
// Given
stubService(forAction: actionType)
// When
mySutFunction()
// Then
verify(mockDelegate, times(1)).showLoading()
}
func testHandleReloadDataCallsShowLoading() {
testMySutFuncionCallsShowLoading ( ()-> sut.handleReloadData())
}
func testHandleViewDidLoadCallsShowLoading() {
testMySutFuncionCallsShowLoading ( ()-> sut.handleViewDidLoad())
}
The DRY principle is not only valid for production code, it can (and should) also be applied to testing code.
Note the described problem has not much to do with the fact that loadData
is a private function, it stays the same when loadData
would become public, or when loadData
would be eliminated by copying its code directly into the calling methods.
-
True, applying DRY principle would help in code repetition, but does not overcome the fact that some test cases are almost replicas of each other.user2037296– user203729609/29/2020 12:40:43Commented Sep 29, 2020 at 12:40
-
That's a flaw on the design. If all the use-cases are practically the same but with little difference and you don't decouple such differences, don't expect testing code to do miracles. The same way, don't expect testing code to do miracles if all your objects are black boxes because the only way to test their behaviour is by auditing the side effects. If any, and pray for those to be deterministicLaiv– Laiv09/29/2020 12:43:46Commented Sep 29, 2020 at 12:43
-
For this specific case, a possible way to validate the final result could be validating the final state of the object under test. Hard if there's no way to do this because of its interface. But not all is lost. I have seen assertions comparing toStrings() because we can determine how will be they. Is it a brittle test? Yes, but has commented, testing code doesn't do miracles. Testability is still a property of any design. A good one to have.Laiv– Laiv09/29/2020 12:51:15Commented Sep 29, 2020 at 12:51
-
@user2037296: see my edit. Feel free to change this to valid swift syntax.Doc Brown– Doc Brown09/29/2020 13:08:57Commented Sep 29, 2020 at 13:08
-
DocBrown & @user2037296: I like this solution (and I don't want to write my own answer because I'd suggest pretty much the same thing), it's just that the way tests are organized (as seen in the question), I'd use the lambda/closure expression to instead inject the call to
verify(mockDelegate, times(1)).someMethod()
, as that's the bit that varies within the same test file (e.g., tests for handleViewDidLoad). Or you could inject both, and maybe leverage Swift's trailing closure syntax (what you get is more or less the Template method pattern, but with functions).Filip Milovanović– Filip Milovanović09/30/2020 08:12:21Commented Sep 30, 2020 at 8:12
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.