1

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:

  1. Check that showLoading is called.
  2. Check that hideLoading is called in case of service success.
  3. Check that hideLoading is called in case of service failure.
  4. 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()
}
7
  • It may be helpful if you share your test code. I don't understand why a test would check that a method is called. The tests should set up the state of the system, call a method, and then check the end state of the system against expectations. I don't think that asserting that a method was called is a meaningful test. Without more information, it seems like the initial state of the system is different for handleViewDidLoad versus handleReloadData, so even if the assertions about the end state are the same, the preconditions are different.
    – Thomas Owens
    Sep 28, 2020 at 22:00
  • 2
    Asserting that a method called is really common. If the behavior of a function is that it needs to call a function to behave correctly, then its correct to test that function is called. For example, testing a conditional decorator. Or testing code that emits an event. You may not want to test the actual receiver, as that's an integration test and may be difficult to set up. Or you may not know who the reciever(s) are. Sep 29, 2020 at 2:48
  • 2
    @ThomasOwens; did you notice the mentioned checks are about (probably injected) delegates? So whether these delegates are called, or not, is part of the external behaviour of that class.
    – Doc Brown
    Sep 29, 2020 at 7:34
  • @ThomasOwens checking that a delegate method is called is an expectation, and it would affect the behavior of the system, if the call was removed. For example, if the handleViewDidLoad was called and hideLoading 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. Sep 29, 2020 at 12:11
  • @ThomasOwens I have edited my question, and added sample tests. Sep 29, 2020 at 12:30

2 Answers 2

7

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.

6
  • 1
    This 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
    Sep 29, 2020 at 7:37
  • Maybe, because testing code can not address a design flaw, it just makes it obvious
    – Laiv
    Sep 29, 2020 at 12:57
  • 1
    @Laiv: not everything we don't understand immediately in full is a design flaw.
    – Doc Brown
    Sep 29, 2020 at 13:10
  • @DocBrown Fair enough.
    – Laiv
    Sep 29, 2020 at 13:43
  • @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. Sep 30, 2020 at 7:57
4

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.

5
  • 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. 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 deterministic
    – Laiv
    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
    Sep 29, 2020 at 12:51
  • @user2037296: see my edit. Feel free to change this to valid swift syntax.
    – Doc Brown
    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). Sep 30, 2020 at 8:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.