1

I'm writing unit tests for various components of a UI. However, I'm running into trouble when writing tests for buttons that trigger asynchronous functions. My problem is that I'm using the UIButton.sendActions(for controlEvents: UIControlEvents) to trigger the pressing of the button, which then calls an asynchronous function.

Say I have a test:

func testLoginToMainScene() {
     loadView()
     let queue = DispatchQueue(label: "LoginButtonPressed")

     queue.sync {
          view.loginButton.sendActions(for: .touchUpInside)
     }

     XCTAssertTrue(router.navigateToMainSceneCalled)
}

That tests the following bit of code in a LoginViewController class:

@IBAction func loginButtonPressed(_ sender: AnyObject) {
     hideKeyboard()
     performLogin(email: emailTextField.text, password: passwordTextField.text)
}

And a function that handles the login by calling a method of a redux worker:

private func performLogin(email: String, password: String) {
     let result = myReduxWorker.getStore().dispatch(newLoginAction(email: email, password: password)

     if let promise = result as? Promise<[String: Any]> {
          promise.done { json -> Void in
               //Login was successful!
               router.navigateToMainScene()
          }
     }

Currently, the test fails because the XCTAssertTrue test runs before the performLogin function completes, and thus before navigateToMainScene is called. I tried using a DispatchQueue, but as soon as the .touchUpInside action is sent to the button, the block of code inside the .sync finishes, and the test function continues and runs the XCTAssertTrue test.

What is the best way to ensure that the performLogin function has finished running before executing the test case?

iamosami
  • 13
  • 2
  • Do you know about XCTestExpectation? – matt Jun 08 '18 at 17:20
  • Possible duplicate of https://stackoverflow.com/questions/29504712/how-can-i-get-xctest-to-wait-for-async-calls-in-setup-before-tests-are-run – matt Jun 08 '18 at 17:21
  • @matt Yes, I tried using expectations but the `sendActions` function is a "fire and forget" type, there is no completion handler to hook into to fulfill the expectation. – iamosami Jun 08 '18 at 17:27
  • It's almost like you're writing a unit test when you should be writing a Ui test. – matt Jun 08 '18 at 17:30
  • Another way of looking at it is: the fact that you are experiencing this issue is a Bad Smell. You are testing the wrong thing, the wrong way. Unit tests are for business logic. You should not be "pushing" any buttons. Do not test what's known to work; do not test the Cocoa framework behavior. – matt Jun 08 '18 at 17:40

1 Answers1

1

What is the best way to ensure that the performLogin function has finished running before executing the test case?

In general, the best way would be for your test to call the performLogin function. Do not use unit tests to trigger or test interface behavior. Test only business logic, and separate out that business logic in such a way as to make it testable.

In your case, however, it may be that what you should have written here all along was a UI test, not a unit test at all. (I can't really say, because I don't know what it is that you are thinking about this situation is supposed to be testable at all.)

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • You're probably right. Testing that the UIButton does what it needs to is an unnecessary step as what I'm really trying to test is `performLogin`. There is some extra code that I left out which is what actually needs to be tested, but it wasn't important for the question. Thanks for the help! – iamosami Jun 08 '18 at 18:00