We have one more delegate method, textFieldShouldReturn(_:). It does two things:
When the user presses the return key in the username field, the input focus should move to the password field.
When the user presses the return key in the password field, the keyboard should be dismissed, and the login process should start.
Let’s start with the second behavior (logging in) before moving to the challenge of testing keyboard input focus. Add a test that populates the text fields, and confirms that pressing the return key in the password field starts the login process. As we did in Test Delegate Methods, we’ll ask the text field for its delegate and talk to it:
| func test_shouldReturn_withPassword_shouldPerformLogin() { |
| sut.usernameField.text = "USERNAME" |
| sut.passwordField.text = "PASSWORD" |
| |
| _ = sut.passwordField.delegate?.textFieldShouldReturn?(sut.passwordField) |
| |
| // Normally, assert something |
| } |
Run the tests. Our pretend login method only prints to the console, so we’ll check that instead of asserting as we normally would. Following Examine Console Output, drill down in the test results until you find the output for this test. You should see this:
| Username: USERNAME |
| Password: PASSWORD |
Next, let’s extract a helper for this delegate method. Most tests can ignore this delegate method’s return value, so declare this helper with @discardableResult:
| @discardableResult func shouldReturn(in textField: UITextField) -> Bool? { |
| textField.delegate?.textFieldShouldReturn?(textField) |
| } |
Using this helper, it’s easier to write tests that check the return key behavior:
| func test_shouldReturn_withPassword_shouldPerformLogin() { |
| sut.usernameField.text = "USERNAME" |
| sut.passwordField.text = "PASSWORD" |
| |
| shouldReturn(in: sut.passwordField) |
| |
| // Normally, assert something |
| } |
With this test in place, let’s now try to test the input focus. Pressing the return key in the username field should move the focus to the password field by calling becomeFirstResponder. So you’d think we’d be able to test the isFirstResponder property as follows:
| func test_shouldReturn_withUsername_shouldMoveInputFocusToPassword() { |
| shouldReturn(in: sut.usernameField) |
| |
| XCTAssertTrue(sut.passwordField.isFirstResponder) |
| } |
Run the tests...but this one will fail. Unfortunately, this test won’t work without some extra help. For changes to the first responder to take effect, UIKit needs the view to live inside a view hierarchy. Add the following helper:
| func putInViewHierarchy(_ vc: UIViewController) { |
| let window = UIWindow() |
| window.addSubview(vc.view) |
| } |
Let’s put the view controller in a view hierarchy in the test’s Arrange section:
| func test_shouldReturn_withUsername_shouldMoveInputFocusToPassword() { |
» | putInViewHierarchy(sut) |
| |
| shouldReturn(in: sut.usernameField) |
| |
| XCTAssertTrue(sut.passwordField.isFirstResponder) |
| } |
Run the tests. This time, the latest test passes.
![]() |
To test keyboard input focus, add the view controller’s view to a UIWindow beforehand. |
To test that a text field resigns the first responder, we need to make it the first responder in the Arrange section. Now we can write a test that puts the focus on the password field and confirms that pressing the return key removes that focus:
| func test_shouldReturn_withPassword_shouldDismissKeyboard() { |
| putInViewHierarchy(sut) |
| sut.passwordField.becomeFirstResponder() |
| XCTAssertTrue(sut.passwordField.isFirstResponder, "precondition") |
| |
| shouldReturn(in: sut.passwordField) |
| |
| XCTAssertFalse(sut.passwordField.isFirstResponder) |
| } |
It’s sometimes helpful to put an assertion in the Arrange section to confirm a precondition. This essentially acts as a test of the putFocusOn(textField:) helper method. By mirroring the opposite assertion at the end, we can clearly see that the lines in between caused this state to change.
Unfortunately, adding a view controller’s view to a window keeps it in memory past the lifetime of the test. To see this, add the following to ViewController:
| deinit { |
| print("ViewController.deinit") |
| } |
Run all tests, then check the console output. You’ll see ViewController.deinit sprinkled here and there. But check the very bottom of the output after all test have completed.
| Test Suite 'All tests' passed at 2019-05-30 17:13:17.930. |
| Executed 11 tests, with 0 failures (0 unexpected) in 0.035 (0.041) seconds |
| ViewController.deinit |
As you can see, a stray view controller has managed to stay alive past the end of all tests. This violates the clean room goal of Chapter 2, Manage Your Test Life Cycles. Drill into the console output for test_shouldReturn_withPassword_shouldDismissKeyboard and you’ll see that it’s missing the expected deinit logging.
As we did with Test Segue-Based Push Navigation, UIKit will clean up the window if we give the run loop a kick. To make it easier to execute the run loop one time, add the following helper:
| func executeRunLoop() { |
| RunLoop.current.run(until: Date()) |
| } |
Then execute the run loop in tearDown after setting everything in the test fixture to nil.
| override func tearDown() { |
» | executeRunLoop() |
| sut = nil |
| super.tearDown() |
| } |
We need to do this whenever any test code calls the putInViewHierarchy helper. Run tests one more time to confirm that this fixes the memory problem.