Test Attributes and Wrangle UIKit Descriptions

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:

TextField/TextFieldTests/ViewControllerTests.swift
 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:

TextField/TextFieldTests/TestHelpers.swift
 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:

TextField/TextFieldTests/TestHelpers.swift
 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:

TextField/TextFieldTests/TestHelpers.swift
 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:

TextField/TextFieldTests/TestHelpers.swift
 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:

TextField/TextFieldTests/TestHelpers.swift
 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.

images/aside-icons/tip.png

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:

TextField/TextFieldTests/ViewControllerTests.swift
 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?.

TextField/TextFieldTests/ViewControllerTests.swift
 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.