Next, let’s add a test to confirm the attributes of the username text field. But testing every single attribute is overkill. Let’s only test the attributes we changed from their default settings. Add a test named test_usernameField_attributesShouldBeSet. Copy and paste the content of the outlet test, except for its assertions. Then add the following assertions and run the tests:
| let textField = sut.usernameField! |
| XCTAssertEqual(textField.textContentType, .username, "textContentType") |
| XCTAssertEqual(textField.autocorrectionType, .no, "autocorrectionType") |
| XCTAssertEqual(textField.returnKeyType, .next, "returnKeyType") |
The tests pass. But let’s see what the failure messages look like. We’ll do this by following Check the Effectiveness of Failure Messages and breaking the production code on purpose. Open Main.storyboard. Show the Attributes Inspector by selecting View ▶ Inspectors ▶ Show Attributes Inspector from the Xcode menu or press ⌥-⌘-4. Select the first text field. In the Text Input Traits section of the Attributes Inspector, change “Content Type” from “Username” to “Password.” Run the tests, and you’ll see this failure message:
| XCTAssertEqual failed: |
| ("Optional(__C.UITextContentType(_rawValue: password))") is not equal to |
| ("Optional(__C.UITextContentType(_rawValue: username))") - textContentType |
This is pretty noisy. Let’s see if we can improve on it by digging into the underlying type. Place your cursor in textContentType, then select Navigate ▶ Jump to Definition from the Xcode menu or press ⌃-⌘-J. This will show you UIKit’s definition of the property.
| @available(iOS 10.0, *) |
| optional public var textContentType: UITextContentType! { get set } |
| // default is nil |
Now place your cursor in UITextContentType and jump to its definition. Here’s what you’ll see:
| public struct UITextContentType : Hashable, Equatable, RawRepresentable |
We can see that UITextContentType doesn’t conform to CustomStringConvertible. Recall from Describe Objects upon Failure that CustomStringConvertible defines how a type describes itself in assertions. So let’s extend UITextContentType to conform to this protocol. We’ll make a separate file for test helpers.
In the Project Navigator, select the TextFieldTests group and press ⌘-N to make a new file. Select Swift File, name it TestHelpers.swift, and set its target to the test target. Add an import UIKit declaration to the top, then add the following empty extension:
| extension UITextContentType: CustomStringConvertible { |
| } |
Because we haven’t implemented anything yet, Swift will complain that the type doesn’t conform to the protocol. Select Editor ▶ Fix All Issues in the Xcode menu. This will generate the stub. Fill in the rest as shown here:
| public var description: String { rawValue } |
Run the tests again. This time, we’ll get the following message:
| XCTAssertEqual failed: ("Optional(password)") is not equal to |
| ("Optional(username)") - textContentType |
That’s better. Let’s reset the storyboard and move on to the next assertion. Go back to Main.storyboard and press ⌘-Z to undo the change. Select the first text field again. This time, change “Correction” from “No” to “Yes.” Run the tests and look at the failure message:
| XCTAssertEqual failed: ("UITextAutocorrectionType") is not equal to |
| ("UITextAutocorrectionType") - autocorrectionType |
Oh dear. This tells us the type but nothing about the values.
Swift knows how to describe enumerations written in Swift but stumbles over enumerations written in Objective-C. This is especially noticeable for the Cocoa Touch frameworks, which have Objective-C interfaces.
Let’s dig into the underlying type. Place your cursor in autocorrectionType, then select Navigate ▶ Jump to Definition from the Xcode menu or press ⌃-⌘-J. Here is UIKit’s definition of the property:
| optional public var autocorrectionType: UITextAutocorrectionType { get set } |
| // default is UITextAutocorrectionTypeDefault |
Now place your cursor in UITextAutocorrectionType and jump to its definition:
| public enum UITextAutocorrectionType : Int { |
| case `default` |
| case no |
| case yes |
| } |
Once again, we can see that this type doesn’t conform to the CustomStringConvertible protocol. Let’s add an empty extension to our test code:
| extension UITextAutocorrectionType: CustomStringConvertible { |
| } |
This results in the following error:
| Type 'UITextAutocorrectionType' does not conform to protocol |
| 'CustomStringConvertible' |
Select Editor ▶ Fix All Issues in the Xcode menu to generate the method stub. Since we want a description for each case, let’s switch on self:
| public var description: String { |
| switch self { |
| } |
| } |
Now we get this error:
| Switch must be exhaustive |
Again, select Editor ▶ Fix All Issues in the Xcode menu. This gives us all the case statements for the enumeration. For each case (except for Swift 5’s @unknown default), return a string:
| public var description: String { |
| switch self { |
| case .default: |
| return "default" |
| case .no: |
| return "no" |
| case .yes: |
| return "yes" |
| @unknown default: |
| fatalError("Unknown UITextAutocorrectionType") |
| } |
| } |
Run the tests again. This time, we’ll get the following message:
| XCTAssertEqual failed: ("yes") is not equal to ("no") - autocorrectionType |
Now that is a useful failure message.
Be careful when using XCTAssertEqual with types declared in Objective-C. Introduce an error to check the failure message. Where needed, add an extension to make the type conform to the CustomStringConvertible protocol. |
Let’s move on to the last assertion in test_usernameField_attributesShouldBeSet. Undo the previous change in the storyboard and select the first text field. This time, change Return Key from “Next” to “Join.” Run the tests and look at the failure message:
| XCTAssertEqual failed: ("UIReturnKeyType") is not equal to |
| ("UIReturnKeyType") - returnKeyType |
Following the preceding steps, create an extension so that UIReturnKeyType conforms to CustomStringConvertible, describing each case. Run the tests to confirm the improved failure message:
| XCTAssertEqual failed: ("join") is not equal to ("next") - returnKeyType |
We now have duplicate code between the tests to load the view controller. Following Move the SUT into the Test Fixture, let’s extract this. For storyboard-based view controllers, remember to change the type cast from as! to as? to silence the warning about assigning to an optional:
| private var sut: ViewController! |
| |
| override func setUp() { |
| super.setUp() |
| let storyboard = UIStoryboard(name: "Main", bundle: nil) |
| sut = storyboard.instantiateViewController( |
| identifier: String(describing: ViewController.self)) |
| sut.loadViewIfNeeded() |
| } |
| |
| override func tearDown() { |
» | executeRunLoop() |
| sut = nil |
| super.tearDown() |
| } |
Finally, we can test the attributes of the password text field. Let’s do this in another test case. We check each text field in its own test case instead of combining the tests, as explained in Why Not Combine Similar Tests?.
| func test_passwordField_attributesShouldBeSet() { |
| let textField = sut.passwordField! |
| XCTAssertEqual(textField.textContentType, .password, "textContentType") |
| XCTAssertEqual(textField.returnKeyType, .go, "returnKeyType") |
| XCTAssertTrue(textField.isSecureTextEntry, "isSecureTextEntry") |
| } |
Run tests to confirm that this passes. We’ve already improved the types used, so these tests will report useful error messages.
With this, we’ve tested the text field attributes as defined in the storyboard. In the rest of this chapter, let’s see how to test the code. We’ll look at delegate methods, and finish by testing input focus—that is, the first responder.