Search code examples
swiftunit-testingtimerdelay

Delay in unit test


So I have a unit test to test if clinics are being updated every 10 seconds. After 5 seconds, I clear all of the clinics. Then set an expectation that times out after 9 seconds to make sure clinics were updated. Here is my code:

func testRefresh() {

    let expec = expectation(description: "Clinics timer expectation")
    let expec2 = expectation(description: "Clinics timer expectation2")
    expec2.isInverted = true
    let dispatchGroup = DispatchGroup(count: 5)

    dataStore.load()

    wait(for: [expec2], timeout: 5.0) // This is what I am asking about
    self.dataStore.clinicsSignal.fire([])

    dataStore.clinicsSignal.subscribeOnce(with: dispatchGroup) {
        print("clinics signal = \($0)")
        expec.fulfill()
    }

    wait(for: [expec], timeout: 9.0)
    XCTAssertFalse(self.dataStore.clinics.isEmpty)
}

I want to have that delay for 5 seconds. Using an inverted expectation the way I did is the only way I could find to make it work. I just think using an inverted expectation is bad practice.

If I use sleep(5) it stops the whole program for 5 seconds. I have also tried a solution using DispatchQueue.main.asyncAfter like outlined here but to no avail.


Solution

  • I have two suggestions to use together:

    • Use a spy test double to make sure that the service your data store uses to refresh the clinics is called twice
    • Inject the refresh interval to make the tests faster

    Spy test double

    Testing the side effect of the data loading, that it hits the service, could be a way to simplify your test.

    Instead of using different expectations and exercising the system under test in a way that might not be what happens at runtime (the dataStore.clinicsSignal.fire([])) you can just count how many times the service is hit, and assert the value is 2.

    Inject refresh interval

    The approach I would recommend is to inject the time setting for how frequently the clinics should be updated in the class, and then set a low value in the tests.

    After all, I'm guessing what you are interested in is that the update code runs as expected, not every 10 seconds. That is, it should update at the frequency you set.

    You could do this by having the value as a default in the init of your data store, and then override it in the tests.

    The reason I'm suggesting to use a shorter refresh interval is that in the context of unit testing, the faster they run the better it is. You want the feedback loop to be as fast as possible.

    Putting it all together, something more or less like this

    protocol ClinicsService {
      func loadClinics() -> SignalProducer<[Clinics], ClinicsError>
    }
    
    class DataSource {
    
      init(clinicsService: ClinicsService, refreshInterval: TimeInterval = 5) { ... }
    }
    
    // in the tests
    
    class ClinicsServiceSpy: ClinicsService {
    
      private(var) callsCount: Int = 0
    
      func loadClinics() -> SignalProducer<[Clinics], ClinicsError> {
        callsCount += 1
        // return some fake data
      }
    }
    
    func testRefresh() {
      let clinicsServiceSpy = ClinicsServiceSpy()
      let dataStore = DataStore(clinicsService: clinicsServiceSpy, refreshInterval: 0.05)
    
      // This is an async expectation to make sure the call count is the one you expect
      _ = expectation(
        for: NSPredicate(
        block: { input, _ -> Bool in
          guard let spy = input as? ClinicsServiceSpy else { return false }
          return spy.callsCount == 2
        ),
        evaluatedWith: clinicsServiceSpy,
        handler: .none
      )
    
      dataStore.loadData()
    
      waitForExpectations(timeout: .2, handler: nil)
    }
    

    If you also used Nimble to have a more refined expectation API your test could look like this:

    func testRefresh() {
      let clinicsServiceSpy = ClinicsServiceSpy()
      let dataStore = DataStore(clinicsService: clinicsServiceSpy, refreshInterval: 0.05)
    
      dataStore.loadData()
    
      expect(clinicsServiceSpy.callsCount).toEventually(equal(2))
    }
    

    The tradeoff you make in this approach is to make the test more straightforward by writing a bit more code. Whether it's a good tradeoff is up to you do decide.

    I like working in this way because it keeps each component in my system free from implicit dependencies and the test I end up writing are easy to read and work as a living documentation for the software.

    Let me know what you think.