简体   繁体   中英

XCTest - Unable to understand / implement expectations in unit tests (to test aysnc code)

(NOTE - I'm developing for macOS, so please... iOS-specific advice won't help me)

What I'm trying to do: I have an app component that performs a short task on a background thread, and then, if certain conditions are met, asynchronously sends out a notification on the main thread.

NOTE - I am not using NSNotification in my app code. I am using my own custom notification mechanism. So, any solution related to NSNotification is not applicable to me.

I'm writing a unit test for the above mentioned app component, and simply want to check if that notification was indeed sent or not . My test has to be able to wait a second or so to give the notification time to reach its subscriber/observer, before performing an assertion.

I want to be able to test both possible cases in my tests: Both are normal scenarios.

  • Notification was sent.

  • Notification was not sent.

After hours of reading several docs and code samples, I don't understand how to achieve this with expectations.

I just want to wait one second in my test. Is it really this complicated?

  • sleep() doesn't work
  • DispatchQueue.main.asyncAfter(time) doesn't work
  • Timer doesn't work

Here's the app component that needs to be tested, and its unit test:

In the below code, where do I put expectation.fulfill()???

class ComponentBeingTested {

    func methodBeingTested() {

        doSomeWork()
        if certainConditionsAreMet {
             DispatchQueue.main.async {sendOutNotification()}
        }
    }
}

...

class UnitTestForComponentBeingTested: XCTestCase {

    let objectBeingTested = ComponentBeingTested()

    func testMethodBeingTested() {

          let expectation = self.expectation(description: "Notification was sent")

          // Call the code being tested
          objectBeingTested.methodBeingTested()

          // How do I do this with expectations ??? Where does expectation.fulfill() go ?
          waitForOneSecond()

          XCTAssertTrue(notificationSent)      // Assume the value of notificationSent is available

    }
}

Here is an approach

func testMethodBeingTested() {

      // create expectation
      let expectation = self.expectation(description: "Notification was sent")

      // set expectation condition
      var notificationSent = false
      let observer = NotificationCenter.default
            .addObserver(forName: _Your_Notification_Name, object: nil, queue: nil) { _ in
            notificationSent = true
            expectation.fulfill()
        }

      // Call the code being tested
      objectBeingTested.methodBeingTested()

      // wait for expectation
      self.wait(for: [expectation], timeout: 5)

      XCTAssertTrue(notificationSent)
}

Check out XCTNSNotificationExpectation , which becomes fulfilled when a matching notification is posted. Different initializers are available, depending on how restrictive you want to be on the fulfilment of the expectation.

To check that the notification is not sent, set isInverted to true on the expectation object.

Then just add a call to waitForExpectations(timeout:handler:) at the end of your test.

Ok, after a lot of trial and error, this works great for me:

Description : I basically created a helper function in my test case class that contains all the boilerplate expectation/wait code. It does the following:

1 - Creates an expectation (ie XCTestExpectation) as a formality.

2 - Calls my (arbitrary) test case assertion code (passed in as a closure) on some global queue thread after the intended delay period. Once this assertion code has completed, the expectation is fulfilled (again, a formality).

3 - Waits on the expectation by calling XCTestCase.wait(timeout). This ensures that the main thread / run loop is kept alive while my assertion code completes on this other thread.

Then, in my test case, I simply invoke that helper function, providing it with a wait period and some code to execute (ie my assertions).

This way, I have a simple and expressive reusable function that hides all the excessive ugliness of expectations which I never thought necessary in the first place.

I can put this helper in a base class like MyAppTestCase: XCTestCase, so that it is available to all my test case classes.

NOTE - This solution can be enhanced and made even more generic/reusable, but as of now, this is quite sufficient for the purposes of the originally posted problem.

Solution :

class ComponentBeingTested {

    func methodBeingTested() {

        doSomeWork()
        if certainConditionsAreMet {
             DispatchQueue.main.async {sendOutNotification()}
        }
    }
}

...

class UnitTestForComponentBeingTested: XCTestCase {

    let objectBeingTested = ComponentBeingTested()

    // Helper function that uses expectation/wait to execute arbitrary
    // test code (passed in as a closure) after some delay period.
    func executeAfter(_ timeSeconds: Double, _ work: (@escaping () -> Void)) {

        let theExpectation = expectation(description: "some expectation")

        // Execute work() after timeSeconds seconds
        DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + timeSeconds) {

            // The call to work() will execute my test assertions
            work()

            // As a formality, fulfill the expectation
            theExpectation.fulfill()
        }

        // Wait for (timeSeconds + 1) seconds to give the work() call 
        // some time to perform the assertions
        wait(for: [theExpectation], timeout: timeSeconds + 1)
    }

    func testMethodBeingTested() {

          // Call the code being tested
          objectBeingTested.methodBeingTested()

          // Call the helper function above, to do the waiting before executing
          // the assertions
          executeAfter(0.5) {

              // Assume the value of notificationSent is computed elsewhere
              // and is available to assert at this point
              XCTAssertTrue(notificationSent)      
          }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM