Let’s begin writing tests for ChangePasswordViewController. If RefactoringTests.swift is still lingering around from creating the project, delete the file. Add a new test suite named ChangePasswordViewControllerTests. Do the normal Test Zero steps to confirm that it’s in the right place.
Then let’s start easy by testing the outlets. Since we know we’re going to be writing many tests, let’s put the system under test into the test fixture from the start. Here’s the content of the file so far:
| @testable import Refactoring |
| import XCTest |
| |
| final class ChangePasswordViewControllerTests: XCTestCase { |
| private var sut: ChangePasswordViewController! |
| |
| override func setUp() { |
| super.setUp() |
| let storyboard = UIStoryboard(name: "Main", bundle: nil) |
| sut = storyboard.instantiateViewController( |
| identifier: String( |
| describing: ChangePasswordViewController.self)) |
| sut.loadViewIfNeeded() |
| } |
| |
| override func tearDown() { |
| sut = nil |
| super.tearDown() |
| } |
| |
| func test_outlets_shouldBeConnected() { |
| XCTAssertNotNil(sut.cancelBarButton, "cancelButton") |
| XCTAssertNotNil(sut.oldPasswordTextField, "oldPasswordTextField") |
| XCTAssertNotNil(sut.newPasswordTextField, "newPasswordTextField") |
| XCTAssertNotNil(sut.confirmPasswordTextField, |
| "confirmPasswordTextField") |
| XCTAssertNotNil(sut.submitButton, "submitButton") |
| } |
| } |
To get this to pass, we edit the view controller in Main.storyboard, copying the Class name into the Storyboard ID.
Next, let’s test the attributes of each UI component from top to bottom. It would be nice to test the title shown in the navigation bar, but there’s no way for tests to reach it. To provide that access, let’s add an outlet to ChangePasswordViewController:
| @IBOutlet private(set) var navigationBar: UINavigationBar! |
In test_outlets_shouldBeConnected, add an assertion for the new outlet:
| XCTAssertNotNil(sut.navigationBar, "navigationBar") |
This test will fail. Get it to pass by connecting the navigation bar to the outlet. Then we can write the test we wanted in the first place, checking the title:
| func test_navigationBar_shouldHaveTitle() { |
| XCTAssertEqual(sut.navigationBar.topItem?.title, "Change Password") |
| } |
Moving down, how can we check the “System Item” setting of the Bar Button Item? UIKit doesn’t expose it as a property, but we can still peek at it since it’s Objective-C underneath. (Peering into Apple’s implementation details is inadvisable in production code. But in test code, it’s a necessary evil when Apple chooses not to expose something that would be useful for tests.)
Add a function to TestHelpers.swift, which uses value(forKey:) to ask a UIBarButtonItem for a hidden numeric property named systemItem. It converts that number into the UIBarButtonItem.SystemItem enumeration.
| func systemItem(for barButtonItem: UIBarButtonItem) -> |
| UIBarButtonItem.SystemItem { |
| let systemItemNumber = barButtonItem.value(forKey: "systemItem") as! Int |
| return UIBarButtonItem.SystemItem(rawValue: systemItemNumber)! |
| } |
We’ll use this helper to test the Cancel button is the right kind of system item.
| func test_cancelBarButton_shouldBeSystemItemCancel() { |
| XCTAssertEqual(systemItem(for: sut.cancelBarButton), .cancel) |
| } |
The tests should pass. But remember the warning from Test Attributes and Wrangle UIKit Descriptions about older enumerations defined in Objective-C: they don’t report mismatches well in Swift tests. To test this, edit the Bar Button Item in the storyboard. Temporarily change its system item to Done instead of Cancel, and run the tests again. You’ll see an unhelpful test failure message:
| XCTAssertEqual failed: ("UIBarButtonSystemItem") is not equal to |
| ("UIBarButtonSystemItem") |
To make the error reporting useful, let’s have the system item conform to the CustomStringConvertible protocol. Add the following to TestHelpers.swift:
| extension UIBarButtonItem.SystemItem: CustomStringConvertible { |
| public var description: String { |
| switch self { |
| case .done: return "done" |
| case .cancel: return "cancel" |
| case .edit: return "edit" |
| case .save: return "save" |
| case .add: return "add" |
| case .flexibleSpace: return "flexibleSpace" |
| case .fixedSpace: return "fixedSpace" |
| case .compose: return "compose" |
| case .reply: return "reply" |
| case .action: return "action" |
| case .organize: return "organize" |
| case .bookmarks: return "bookmarks" |
| case .search: return "search" |
| case .refresh: return "refresh" |
| case .stop: return "stop" |
| case .camera: return "camera" |
| case .trash: return "trash" |
| case .play: return "play" |
| case .pause: return "pause" |
| case .rewind: return "rewind" |
| case .fastForward: return "fastForward" |
| case .undo: return "undo" |
| case .redo: return "redo" |
| case .pageCurl: return "pageCurl" |
| case .close: return "close" |
| @unknown default: fatalError("Unknown UIBarButtonItem.SystemItem") |
| } |
| } |
| } |
Run tests again. This time, you should see the following test failure:
| XCTAssertEqual failed: ("done") is not equal to ("cancel") |
That’s more like it. Change the system item back to Cancel. Run tests again to make sure we’re back to a clean state.
Tests for the text field placeholders and the Submit button title are straightforward:
| func test_oldPasswordTextField_shouldHavePlaceholder() { |
| XCTAssertEqual(sut.oldPasswordTextField.placeholder, "Current Password") |
| } |
| |
| func test_newPasswordTextField_shouldHavePlaceholder() { |
| XCTAssertEqual(sut.newPasswordTextField.placeholder, "New Password") |
| } |
| |
| func test_confirmPasswordTextField_shouldHavePlaceholder() { |
| XCTAssertEqual(sut.confirmPasswordTextField.placeholder, |
| "Confirm New Password") |
| } |
| |
| func test_submitButton_shouldHaveTitle() { |
| XCTAssertEqual(sut.submitButton.titleLabel?.text, "Submit") |
| } |
The Current Password text field is set up to receive password input and to enable the Return key. Let’s write a test that confirms those attributes:
| func test_oldPasswordTextField_shouldHavePasswordAttributes() { |
| let textField = sut.oldPasswordTextField! |
| XCTAssertEqual(textField.textContentType, .password, "textContentType") |
| XCTAssertTrue(textField.isSecureTextEntry, "isSecureTextEntry") |
| XCTAssertTrue(textField.enablesReturnKeyAutomatically, |
| "enablesReturnKeyAutomatically") |
| } |
Make similar tests for newPasswordTextField and confirmPasswordTextField but change their textContentType assertions to check for .newPassword.
Now we have tests for the basic configuration of the UI elements on the Change Password view controller. Next, let’s begin adding tests of behavior, starting with button taps.