Test-Driven Development in Swift
In this blog post, we will learn how to build a simple iOS app menu (shown below) using Test-Driven Development in Swift.
Here are things you need to know to fully understand the concepts presented in this post:
- Xcode 6
- Familiarity with basic concepts in Swift
- Familiarity with commonly used classes in UIKit and Foundation (e.g., UITableView and NSNotificationCenter)
- Familiarity with XCTest. If you have never used XCTest before, please read the XCTestCase section from Matt Thompson’s blog post on the topic.
Create a new iOS project in Xcode 6. Select the Single View Application template. Use AppMenu for Product Name, Swift as the language, and iPhone for Devices. Make sure the Use Core Data check box is not selected. For this exercise we won’t be using Storyboards. So delete the Main.storyboard
file. Don’t forget to remove the storyboard name (Main) from the Main Interface drop-down located in Deployment Info section under General tab for AppMenu target. While we are at it, let’s delete ViewController.swift
and AppMenuTests.swift
files as well. They were created by Xcode and we won’t need them.
Before we embark on this beautiful journey of Test-Driven Development (TDD), let’s step back and think about the app design a little bit. You might have heard that TDD is more of a design exercise than a testing activity. If so, you have heard it correct. TDD forces us to use the code even before it exists through tests which in turn forces us to think about how the class under test interacts with the rest of the code in an application. This act of using code we wish we had helps us make (good) design decisions that lead to the creation of reusable classes and easy-to-use application programming interfaces (API). That being said, it is not always guaranteed that an application built by writing tests first is well designed. We still need to apply good design principles and patterns in addition to writing tests first.
The figure below shows the initial design we are aiming for. If you are worried that we might have just stepped into the Big Up Front Design (BUFD) territory, fear not. The design below gives us a starting point. We haven’t figured out every single aspect of the app yet. For example, we don’t know what the public API for each class is going to look like. As we go through the process of building the app, we might realize that we need to change the design below completely and that is perfectly fine.
Identifying Domain Objects #
When I am starting on a new project, I often struggle to find the first good test to write. As a result, I resort to looking for domain objects which are usually easy to test. Our app menu will display information about each menu item for example, title, subtitle, and icon. We need an instance that stores information about a menu item. Let’s call it MenuItem
. We’ll define what information a MenuItem
instance contains through tests.
Create a new file named MenuItemTests.swift
and place it under AppMenuTests
group. Right click on AppMenuTests
group and select New File > iOS > Source > Test Case Class to create a new test class named MenuItemTests
. Make it a subclass of XCTestCase
and select Swift as the language. Delete everything in MenuItemTests.swift
file except the class definition and import statements.
import UIKit
import XCTest
class MenuItemTests: XCTestCase {
}
Our first test will be to make sure that a menu item has a title. Add following test to the MenuItemTests
class.
func testThatMenuItemHasATitle() {
let menuItem = MenuItem(title: "Contributions")
XCTAssertEqual(menuItem.title, "Contributions",
"A title should always be present")
}
In above test, we create an instance of MenuItem
; give it a title; and verify that it holds onto that title by using the XCTAssertEqual
assertion provided by XCTest framework that comes with Xcode. We are also (implicitly) specifying that MenuItem
should provide an initializer that takes title
as a parameter. When we write tests first, we tend to discover subtle details like this about our APIs.
XCTest provides a number of assertions. Each assertion allows you to pass a test description that explains what the test is about. I recommend you always provide this description.
As things stand right now, we are not able to run the test. It doesn’t even compile. We need to create MenuItem
. Before we decide whether we should make MenuItem
a struct or a class, I encourage you to read the Choosing Between Classes and Structures section from The Swift Programming Language Guide first.
At a first glance, struct
might seem sufficient here. However, in Handling Menu Item Tap Event section below we will need to store a menu item in a NSNotification
object. NSNotification
expects an object that needs to be stored in it to conform to AnyObject
protocol. A struct
type doesn’t conform to AnyObject
. Therefore, we need to make MenuItem
a class. Create a new file named MenuItem.swift
(File > New > File > iOS > Source > Swift File). Add it to both AppMenu and AppMenuTests targets and replace its content with following.
import Foundation
class MenuItem {
let title: String
init(title: String) {
self.title = title
}
}
You shouldn’t have to add application classes into test targets, but there seems to be a bug in Xcode 6 that forces you to do so. Otherwise, you will get use of unresolved identifier error. If you receive that error in tests at any point in the future, you can fix it by adding the application class file to the test target as well. You can add a file to a target by clicking the + button from Compile Sources section in Build Phases tab.
Run the test (Product > Test or ⌘U). It should pass. Let’s write a test for the subtitle property next.
func testThatMenuItemCanBeAssignedASubTitle() {
let menuItem = MenuItem(title: "Contributions")
menuItem.subTitle = "Repos contributed to"
XCTAssertEqual(menuItem.subTitle!, "Repos contributed to",
"Subtitle should be what we assigned")
}
It should pass after adding the subTitle
property to MenuItem
class.
class MenuItem {
let title: String
var subTitle: String?
init(title: String) {
self.title = title
}
}
Since a menu item must have a title, it’s defined as a constant property. Whereas a subTitle
is not required. Therefore, we define it as a variable property. Finally, here is a test for the iconName
property:
func testThatMenuItemCanBeAssignedAnIconName() {
let menuItem = MenuItem(title: "Contributions")
menuItem.iconName = "iconContributions"
XCTAssertEqual(menuItem.iconName!, "iconContributions",
"Icon name should be what we assigned")
}
It should pass by adding the iconName
property to MenuItem
.
class MenuItem {
let title: String
var subTitle: String?
var iconName: String?
init(title: String) {
self.title = title
}
}
Before we move on, let’s refactor our tests by moving the MenuItem
creation code into setup
method.
It is a general practice in TDD to refactor once all tests are passing. The process of refactoring helps us improve design in small increments by organizing the code and tests better. It also helps us remove any duplication. This iterative process of writing a failing test first, making it pass with just enough code and improving the design before writing the next failing test is known as red-green-refactor cycle.
class MenuItemTests: XCTestCase {
var menuItem: MenuItem?
override func setUp() {
super.setUp()
menuItem = MenuItem(title: "Contributions")
}
func testThatMenuItemHasATitle() {
XCTAssertEqual(menuItem!.title, "Contributions",
"A title should always be present")
}
func testThatMenuItemCanBeAssignedASubTitle() {
menuItem!.subTitle = "Repos contributed to"
XCTAssertEqual(menuItem!.subTitle!, "Repos contributed to",
"Subtitle should be what we assigned")
}
func testThatMenuItemCanBeAssignedAnIconName() {
menuItem!.iconName = "iconContributions"
XCTAssertEqual(menuItem!.iconName!, "iconContributions",
"Icon name should be what we assigned")
}
}
XCTest calls the setup
method before running each test. When a test is finished running, the variables assigned in setup
method are set to nil
. After that it creates brand new instances of objects and assigns them to those variables in setup
method again. XCTest does this to isolate each test. We don’t want any residual data created by previous tests affect the next ones. As XCTest automatically sets variables to nil
when a test is finished running, we don’t need to explicitly set them to nil
in tearDown
method (also provided by XCTest). That being said, if you need to do any cleanup other than setting variables to nil
, you should do that in tearDown
method.
Reading Metadata from Plist #
Next up we will read the metadata required to create MenuItem
instances from a plist. As our initial design suggests, we will be storing the metadata for each menu item in a plist file. That way if we need to populate the menu dynamically by fetching the metadata from a remote server, we won’t need to make too many changes as long as the metadata format remains the same. Before building MenuItemsPlistReader
class, we need to know how the MenuItemsReader
protocol looks like. Here is my initial pass at it:
import Foundation
protocol MenuItemsReader {
func readMenuItems() -> ([[String : String]]?, NSError?)
}
readMenuItems
method doesn’t take any parameters and returns a tuple. The first item in the tuple contains an array of dictionaries if the file was read successfully. The second item contains an NSError object if the file couldn’t be read. readMenuItems
is a required method. So any class that wants to conform to MenuItemsReader
protocol, must implement it. Create a new file named MenuItemsReader.swift
. Add it to both targets and then replace its content with the protocol definition code listed above.
Let’s read the metadata from a plist file next. We will write tests first. Create a new file named MenuItemsPlistReaderTests.swift
in AppMenuTests
target. Now that you know how to create a Swift test file, I will skip those instructions moving forward. Delete everything in MenuItemsPlistReaderTests.swift
file except the class definition and import statements. Our first test will be to make sure that MenuItemsPlistReader
returns an error if it can’t read the specified plist file. Add following test to the MenuItemsPlistReaderTests
class.
func testErrorIsReturnedWhenPlistFileDoesNotExist() {
let plistReader = MenuItemsPlistReader()
plistReader.plistToReadFrom = "notFound"
let (metadata, error) = plistReader.readMenuItems()
XCTAssertNotNil(error, "Error is returned when plist doesn't exist")
}
We create an instance of MenuItemsPlistReader
; give it a non-existent plist file name to read from; and call readMenuItems
method. Then we verify that it returns an error. To make the test pass, we need to create MenuItemsPlistReader
class and add it to both targets. Replace its content with following.
import Foundation
class MenuItemsPlistReader: MenuItemsReader {
var plistToReadFrom: String? = nil
func readMenuItems() -> ([[String : String]]?, NSError?) {
let error = NSError(domain: "Some domain",
code: 0,
userInfo: nil)
return ([], error)
}
}
Now run the test. It should pass. Although the test passes, something doesn’t look right. readMenuItems
doesn’t even attempt to read the file. It always returns a tuple containing an empty array and not-so-useful error. This brings us to an important aspect of TDD: write minimum code to pass the test. Being disciplined about not writing anymore code than necessary to pass the tests is key to TDD. Therefore, we won’t be fixing the simingly broken readMenuItems
method unless our tests require us to do so.
The only requirement we have defined for MenuItemsPlistReader
class so far is that it returns an error if the file doesn’t exist. We haven’t specified what should be in that error object. Let’s add a couple more tests to make sure the error contains the domain, code and description we are expecting.
Apple recommends that we use NSError objects to capture information about runtime errors. These objects should contain the error domain, a domain-specific error code, and a user info dictionary containing the error description. You can add other details about the error in user info dictionary, for example what steps to take to resolve the error.
func testCorrectErrorDomainIsReturnedWhenPlistDoesNotExist() {
let plistReader = MenuItemsPlistReader()
plistReader.plistToReadFrom = "notFound"
let (metadata, error) = plistReader.readMenuItems()
let errorDomain = error?.domain
XCTAssertEqual(errorDomain!, MenuItemsPlistReaderErrorDomain,
"Correct error domain is returned")
}
func testFileNotFoundErrorCodeIsReturnedWhenPlistDoesNotExist() {
let plistReader = MenuItemsPlistReader()
plistReader.plistToReadFrom = "notFound"
let (metadata, error) = plistReader.readMenuItems()
let errorCode = error?.code
XCTAssertEqual(errorCode!,
MenuItemsPlistReaderErrorCode.FileNotFound.toRaw(),
"Correct error code is returned")
}
func testCorrectErrorDescriptionIsReturnedWhenPlistDoesNotExist() {
let plistReader = MenuItemsPlistReader()
plistReader.plistToReadFrom = "notFound"
let (metadata, error) = plistReader.readMenuItems()
let userInfo = error?.userInfo
let description: String =
userInfo![NSLocalizedDescriptionKey]! as String
XCTAssertEqual(description,
"notFound.plist file doesn't exist in app bundle",
"Correct error description is returned")
}
Following changes to MenuItemsPlistReader
should make the above tests pass.
import Foundation
let MenuItemsPlistReaderErrorDomain = "MenuItemsPlistReaderErrorDomain"
enum MenuItemsPlistReaderErrorCode : Int {
case FileNotFound
}
class MenuItemsPlistReader: MenuItemsReader {
var plistToReadFrom: String? = nil
func readMenuItems() -> ([[String : String]]?, NSError?) {
let errorMessage =
"\(plistToReadFrom!).plist file doesn't exist in app bundle"
let userInfo = [NSLocalizedDescriptionKey: errorMessage]
let error = NSError(domain: MenuItemsPlistReaderErrorDomain,
code: MenuItemsPlistReaderErrorCode.FileNotFound.toRaw(),
userInfo: userInfo)
return ([], error)
}
}
readMenuItems
method still doesn’t look right. Next tests we are going to write will force us not to cheat. Before we move forward, delete the test named testErrorIsReturnedWhenPlistFileDoesNotExist
. It is made redundant by previous three tests.
func testPlistIsDeserializedCorrectly() {
let plistReader = MenuItemsPlistReader()
plistReader.plistToReadFrom = "menuItems"
let (metadata, error) = plistReader.readMenuItems()
XCTAssertTrue(metadata?.count == 3,
"There should only be three dictionaries in plist")
let firstRow = metadata?[0]
XCTAssertEqual(firstRow!["title"]!, "Contributions",
"First row's title should be what's in plist")
XCTAssertEqual(firstRow!["subTitle"]!, "Repos contributed to",
"First row's subtitle should be what's in plist")
XCTAssertEqual(firstRow!["iconName"]!, "iconContributions",
"First row's icon name should be what's in plist")
let secondRow = metadata?[1]
XCTAssertEqual(secondRow!["title"]!, "Repositories",
"Second row's title should be what's in plist")
XCTAssertEqual(secondRow!["subTitle"]!, "Repos collaborating",
"Second row's subtitle should be what's in plist")
XCTAssertEqual(secondRow!["iconName"]!, "iconRepositories",
"Second row's icon name should be what's in plist")
let thirdRow = metadata?[2]
XCTAssertEqual(thirdRow!["title"]!, "Public Activity",
"Third row's title should be what's in plist")
XCTAssertEqual(thirdRow!["subTitle"]!, "Activity viewable by anyone",
"Third row's subtitle should be what's in plist")
XCTAssertEqual(thirdRow!["iconName"]!, "iconPublicActivity",
"Third row's icon name should be what's in plist")
}
Here we are making sure that readMenuItems
method actually reads data from the specified plist file and creates proper objects from that data.
A rule of thumb while writing unit tests is not to include more than one assertion in a test method. I am violating that rule here, because it makes sense to verify that the data read from the file is correct in one place.
To pass above test, create a file named “menuItems.plist” (Right click AppMenu group > New File > iOS > Resource > Property List). Add it to both targets. Open that file in source code mode (Right click the file in Xcode > Open As > Source Code) and replace its content with following:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>title</key>
<string>Contributions</string>
<key>subTitle</key>
<string>Repos contributed to</string>
<key>iconName</key>
<string>iconContributions</string>
<key>featureName</key>
<string>contributions</string>
</dict>
<dict>
<key>title</key>
<string>Repositories</string>
<key>subTitle</key>
<string>Repos collaborating</string>
<key>iconName</key>
<string>iconRepositories</string>
<key>featureName</key>
<string>repositories</string>
</dict>
<dict>
<key>title</key>
<string>Public Activity</string>
<key>subTitle</key>
<string>Activity viewable by anyone</string>
<key>iconName</key>
<string>iconPublicActivity</string>
<key>featureName</key>
<string>publicActivity</string>
</dict>
</array>
</plist>
Next add following images to the asset catalog (Images.xcassets). They are included in the finished project.
Now modify readMenuItems
method as shown below:
func readMenuItems() -> ([[String : String]]?, NSError?) {
var error: NSError? = nil
var fileContents: [[String : String]]? = nil
let bundle = NSBundle(forClass: object_getClass(self))
if let filePath =
bundle.pathForResource(plistToReadFrom, ofType: "plist")
{
fileContents =
NSArray(contentsOfFile: filePath) as? [[String : String]]
}
else {
let errorMessage =
"\(plistToReadFrom!).plist file doesn't exist in app bundle"
let userInfo = [NSLocalizedDescriptionKey: errorMessage]
error = NSError(domain: MenuItemsPlistReaderErrorDomain,
code: MenuItemsPlistReaderErrorCode.FileNotFound.toRaw(),
userInfo: userInfo)
}
return (fileContents, error)
}
Now that the tests are passing, let’s refactor by extracting the error building code into a separate method. Here is how readMenuItems
method looks like after refactoring:
func readMenuItems() -> ([[String : String]]?, NSError?) {
var error: NSError? = nil
var fileContents: [[String : String]]? = nil
let bundle = NSBundle(forClass: object_getClass(self))
if let filePath =
bundle.pathForResource(plistToReadFrom, ofType: "plist")
{
fileContents =
NSArray(contentsOfFile: filePath) as? [[String : String]]
}
else {
error = fileNotFoundError()
}
return (fileContents, error)
}
func fileNotFoundError() -> NSError {
let errorMessage =
"\(plistToReadFrom!).plist file doesn't exist in app bundle"
let userInfo = [NSLocalizedDescriptionKey: errorMessage]
return NSError(domain: MenuItemsPlistReaderErrorDomain,
code: MenuItemsPlistReaderErrorCode.FileNotFound.toRaw(),
userInfo: userInfo)
}
Run the tests again (⌘U) to make sure that we didn’t break anything. I see some refactoring opportunity with our tests as well. Let’s move the common code from all tests into setup
method.
class MenuItemsPlistReaderTests: XCTestCase {
var plistReader: MenuItemsPlistReader?
var metadata: [[String : String]]?
var error: NSError?
override func setUp() {
super.setUp()
plistReader = MenuItemsPlistReader()
plistReader?.plistToReadFrom = "notFound"
(metadata, error) = plistReader!.readMenuItems()
}
func testCorrectErrorDomainIsReturnedWhenPlistDoesNotExist() {
let errorDomain = error?.domain
XCTAssertEqual(errorDomain!, MenuItemsPlistReaderErrorDomain,
"Correct error domain is returned")
}
func testFileNotFoundErrorCodeIsReturnedWhenPlistDoesNotExist() {
let errorCode = error?.code
XCTAssertEqual(errorCode!,
MenuItemsPlistReaderErrorCode.FileNotFound.toRaw(),
"Correct error code is returned")
}
func testCorrectErrorDescriptionIsReturnedWhenPlistDoesNotExist() {
let userInfo = error?.userInfo
let description: String =
userInfo![NSLocalizedDescriptionKey]! as String
XCTAssertEqual(description,
"notFound.plist file doesn't exist in app bundle",
"Correct error description is returned")
}
func testPlistIsDeserializedCorrectly() {
plistReader!.plistToReadFrom = "menuItems"
(metadata, error) = plistReader!.readMenuItems()
XCTAssertTrue(metadata?.count == 3,
"There should only be three dictionaries in plist")
let firstRow = metadata?[0]
XCTAssertEqual(firstRow!["title"]!, "Contributions",
"First row's title should be what's in plist")
XCTAssertEqual(firstRow!["subTitle"]!, "Repos contributed to",
"First row's subtitle should be what's in plist")
XCTAssertEqual(firstRow!["iconName"]!, "iconContributions",
"First row's icon name should be what's in plist")
let secondRow = metadata?[1]
XCTAssertEqual(secondRow!["title"]!, "Repositories",
"Second row's title should be what's in plist")
XCTAssertEqual(secondRow!["subTitle"]!, "Repos collaborating",
"Second row's subtitle should be what's in plist")
XCTAssertEqual(secondRow!["iconName"]!, "iconRepositories",
"Second row's icon name should be what's in plist")
let thirdRow = metadata?[2]
XCTAssertEqual(thirdRow!["title"]!, "Public Activity",
"Third row's title should be what's in plist")
XCTAssertEqual(thirdRow!["subTitle"]!,
"Activity viewable by anyone",
"Third row's subtitle should be what's in plist")
XCTAssertEqual(thirdRow!["iconName"]!, "iconPublicActivity",
"Third row's icon name should be what's in plist")
}
}
I realized that we forgot to add a test for a scenario when the plist exists, but readMenuItems
is unable to read it perhaps due to bad data. I will leave that as an exercise for you my dear readers.
Building Menu Items #
We are now ready to build MenuItem
instances from the metadata we just read. Create a new test class named MenuItemBuilderTests
and replace its content with following:
import UIKit
import XCTest
class MenuItemBuilderTests: XCTestCase {
var menuItemBuilder: MenuItemBuilder?
var fakeMenuItemsReader: FakeMenuItemsReader?
var menuItems: [MenuItem]?
var error: NSError?
override func setUp() {
fakeMenuItemsReader = FakeMenuItemsReader()
fakeMenuItemsReader!.missingTitle = true
let (metadata, _) = fakeMenuItemsReader!.readMenuItems()
menuItemBuilder = MenuItemBuilder()
(menuItems, error) =
menuItemBuilder!.buildMenuItemsFromMetadata(metadata!)
}
func testCorrectErrorDomainIsReturnedWhenTitleIsMissing() {
let errorDomain = error?.domain
XCTAssertEqual(errorDomain!, MenuItemBuilderErrorDomain,
"Correct error domain is returned")
}
func testMissingTitleErrorCodeIsReturnedWhenTitleIsMissing() {
let errorCode = error?.code
XCTAssertEqual(errorCode!,
MenuItemBuilderErrorCode.MissingTitle.toRaw(),
"Correct error code is returned")
}
func testCorrectErrorDescriptionIsReturnedWhenTitleIsMissing() {
let userInfo = error?.userInfo
let description: String =
userInfo![NSLocalizedDescriptionKey]! as String
XCTAssertEqual(description,
"All menu items must have a title",
"Correct error description is returned")
}
func testEmptyArrayIsReturnedWhenErrorIsPresent() {
XCTAssertTrue(menuItems?.count == 0,
"No menu item instances are returned when error is present")
}
}
A menu item must have a title. Therefore, we need to make sure that MenuItemBuilder
returns an error if the title is missing. We also need to make sure that an empty list of menu items is returned when an error occurs.
In above test, instead of using the real menu items metadata reader (MenuItemsPlistReader
), we are using a fake one called FakeMenuItemsReader
. The reason for that is we need to isolate the class under test from all other components in the app. By doing so when a test fails, we can be reasonably certain that the issue is in the class under test and not someother class that it depends on. Furthermore, if we used the real metadata reader in our tests and if that class decides to download the plist from a remote server in the future, the tests for MenuItemBuilder
will suffer unnecessarily if the download takes a while. We should always aim towards speedy and non-brittle tests that are easy to maintain.
For FakeMenuItemsReader
to be able to stand-in for other menu items readers out there, it must conform to the MenuItemsReader
protocol. Instead of reading the metadata from a file or remote server, it always returns a hard-coded array of dictionaries. Create FakeMenuItemsReader
class and add it only to AppMenuTests
target. We won’t be using this class in any of the application code. Replace the content of FakeMenuItemsReader.swift
file with following:
import Foundation
class FakeMenuItemsReader : MenuItemsReader {
var missingTitle: Bool = false
func readMenuItems() -> ([[String : String]]?, NSError?) {
let menuItem1 =
missingTitle ? menuItem1WithMissingTitle()
: menuItem1WithNoMissingTitle()
let menuItem2 = ["title": "Menu Item 2",
"subTitle": "Menu Item 2 subtitle",
"iconName": "iconName2"]
return ([menuItem1, menuItem2], nil)
}
func menuItem1WithMissingTitle() -> [String : String] {
return ["subTitle": "Menu Item 1 subtitle",
"iconName": "iconName1"]
}
func menuItem1WithNoMissingTitle() -> [String : String] {
var menuItem = menuItem1WithMissingTitle()
menuItem["title"] = "Menu Item 2"
return menuItem
}
}
One concern that often arises with these fake classes is that they might go out of date if the public API of the original classes they are standing in for change. It’s a valid concern. However, since Swift throws a compile time error if a class that claims to conform to a protocol doesn’t actually implement the required methods, we don’t need to worry about it here. For example, if we decided to return a non-optional array of menu items from the readMenuItems
method in MenuItemsReader
protocol we will be forced to apply that change to both MenuItemsPlistReader
and FakeMenuItemsReader
classes. Go ahead, try it. Isn’t Swift great that way?
If you would like to learn more about an “alternate universe” these fake objects might create in your tests with specific examples, please read chapter 9 (Creating Test Doubles section) from Practical Object Oriented Design in Ruby book.
The other concern with fake classes is that they might be giving us a false sense of security. How do we really know that MenuItemsPlistReader
and MenuItemBuilder
work well together? The answer is we don’t, at least through unit tests. The job of making sure that different units of an app work well together is usually given to integration tests, which won’t be covered in this blog post.
Now create a new Swift class named MenuItemBuilder
in AppMenu group. Add it to both targets and replace its content with following:
import Foundation
let MenuItemBuilderErrorDomain = "MenuItemBuilderErrorDomain"
enum MenuItemBuilderErrorCode : Int {
case MissingTitle
}
class MenuItemBuilder {
func buildMenuItemsFromMetadata(metadata: [[String : String]])
-> ([MenuItem]?, NSError?)
{
let userInfo =
[NSLocalizedDescriptionKey: "All menu items must have a title"]
let error = NSError(domain: MenuItemBuilderErrorDomain,
code: MenuItemBuilderErrorCode.MissingTitle.toRaw(),
userInfo: userInfo)
return ([], error)
}
}
We have written just enough code to make the error tests pass. Next we need to make sure that the builder creates correct number of menu items. Add following test to MenuItemBuilderTests
class.
func testOneMenuItemInstanceIsReturnedForEachDictionary() {
fakeMenuItemsReader!.missingTitle = false
let (metadata, _) = fakeMenuItemsReader!.readMenuItems()
(menuItems, _) =
menuItemBuilder!.buildMenuItemsFromMetadata(metadata!)
XCTAssertTrue(menuItems?.count == 2,
"Number of menu items should be equal to number of dictionaries")
}
One feature I particularly like about Swift is that I can easily ignore a return value that I am not interested in by using _
. Following modification to MenuItemBuilder
class should make all tests pass.
class MenuItemBuilder {
func buildMenuItemsFromMetadata(metadata: [[String : String]])
-> ([MenuItem]?, NSError?)
{
var menuItems = [MenuItem]()
var error: NSError?
for dictionary in metadata {
if let title = dictionary["title"] {
let menuItem = MenuItem(title: title)
menuItem.subTitle = dictionary["subTitle"]
menuItem.iconName = dictionary["iconName"]
menuItems.append(menuItem)
}
else {
error = missingTitleError()
menuItems.removeAll(keepCapacity: false)
break
}
}
return (menuItems, error)
}
private func missingTitleError() -> NSError {
let userInfo =
[NSLocalizedDescriptionKey: "All menu items must have a title"]
return NSError(domain: MenuItemBuilderErrorDomain,
code: MenuItemBuilderErrorCode.MissingTitle.toRaw(),
userInfo: userInfo)
}
}
You might have noticed that I didn’t strictly follow the red-green-refactor cycle with above changes. I should be extracting the error building code into a separate method only after all tests are passing. Although, I don’t encourage ignoring the write minimum amount of code to pass tests first rule from TDD, I will be doing exactly so every now and then so as not to make this blog post too long.
The last test for MenuItemBuilder
is to verify that it populates the menu item instances’ properties with values present in metadata dictionaries.
func testMenuItemPropertiesContainValuesPresentInDictionary() {
fakeMenuItemsReader!.missingTitle = false
let (metadata, _) = fakeMenuItemsReader!.readMenuItems()
(menuItems, _) =
menuItemBuilder!.buildMenuItemsFromMetadata(metadata!)
let rawDictionary1 = metadata![0]
let menuItem1 = menuItems![0]
XCTAssertEqual(menuItem1.title,
rawDictionary1["title"]!,
"1st menu item's title should be what's in the 1st dictionary")
XCTAssertEqual(menuItem1.subTitle!,
rawDictionary1["subTitle"]!,
"1st menu item's subTitle should be what's in the 1st dictionary")
XCTAssertEqual(menuItem1.iconName!,
rawDictionary1["iconName"]!,
"1st menu item's icon name should be what's in the 1st dictionary")
let rawDictionary2 = metadata![1]
let menuItem2 = menuItems![1]
XCTAssertEqual(menuItem2.title,
rawDictionary2["title"]!,
"2nd menu item's title should be what's in the 2nd dictionary")
XCTAssertEqual(menuItem2.subTitle!,
rawDictionary2["subTitle"]!,
"2nd menu item's subTitle should be what's in the 2nd dictionary")
XCTAssertEqual(menuItem2.iconName!,
rawDictionary2["iconName"]!,
"2nd menu item's icon name should be what's in the 2nd dictionary")
}
Once again, we are using multiple assertions within a test here. The test above should pass without any code changes.
Displaying Menu Items #
Now that we have a way to build MenuItem
instances and populate them with information present in a plist, let’s move our focus to displaying their content. We will be using a table view to display the menu items. As our initial design suggests, MenuTableDefaultDataSource
will be responsible for providing a fully configured UITableViewCell
for each menu item. The table view itself is managed by MenuViewController
.
Providing Data to Table View #
We will use a separate object as a data source for the table view instead of giving MenuViewController
that responsibility. MenuViewController
is already reponsible for managing the views. I would hate to violate the Single Responsibility Principle by also making it prepare data for the table view. But first we will create a protocol that MenuTableDefaultDataSource
will conform to. Create a new Swift file named MenuTableDataSource.swift
in AppMenu group. Add it to both targets and replace its content with following:
import UIKit
protocol MenuTableDataSource : UITableViewDataSource {
func setMenuItems(menuItems: [MenuItem])
}
MenuTableDataSource
is a protocol that inherits from UITableViewDataSource
. It also introduces a required method named setMenuItems
. Now we are ready to write tests for MenuTableDefaultDataSource
. Create a new test file named MenuTableDefaultDataSourceTests.swift
in AppMenuTests
target and replace its content with following.
import UIKit
import XCTest
class MenuTableDefaultDataSourceTests: XCTestCase {
func testReturnsOneRowForOneMenuItem() {
let testMenuItem = MenuItem(title: "Test menu item")
let menuItemsList = [testMenuItem]
let dataSource = MenuTableDefaultDataSource()
dataSource.setMenuItems(menuItemsList)
let numberOfRows =
dataSource.tableView(nil, numberOfRowsInSection:0)
XCTAssertEqual(numberOfRows,
menuItemsList.count,
"Only 1 row is returned since we're passing 1 menu item")
}
}
Here we are verifying that the data source creates one table view cell instance for each menu item. Now create a new Swift file named MenuTableDefaultDataSource.swift
and replace its content with the code below.
import Foundation
import UIKit
class MenuTableDefaultDataSource : NSObject, MenuTableDataSource {
var menuItems: [MenuItem]?
func setMenuItems(menuItems: [MenuItem]) {
self.menuItems = menuItems
}
func tableView(tableView: UITableView!,
numberOfRowsInSection section: Int)
-> Int
{
return 1
}
func tableView(tableView: UITableView!,
cellForRowAtIndexPath indexPath: NSIndexPath!)
-> UITableViewCell!
{
return nil;
}
}
Although we haven’t written tests for tableView:cellForRowAtIndexPath:
method yet, we need to implement it in order to run the previous test. It happens to be a required method in UITableViewDataSource
protocol and Swift won’t compile MenuTableDefaultDataSource
without it.
One other thing you might have noticed about MenuTableDefaultDataSource
is that it inherits from NSObject
. The reason for that is in order to conform to UITableViewDataSource
protocol, it needs to conform to NSObject
protocol as well. An easy way to accomplish that is by making it a subclass of NSObject
which already conforms to the NSObject
protocol.
In above test, we are cheating by returning 1
from tableView:numberOfRowsInSection:
method. Add one more test to verify that the data source always returns correct number of rows no matter how many menu items are present.
func testReturnsTwoRowsForTwoMenuItems() {
let testMenuItem1 = MenuItem(title: "Test menu item 1")
let testMenuItem2 = MenuItem(title: "Test menu item 2")
let menuItemsList = [testMenuItem1, testMenuItem2]
let dataSource = MenuTableDefaultDataSource()
dataSource.setMenuItems(menuItemsList)
let numberOfRows =
dataSource.tableView(nil, numberOfRowsInSection:0)
XCTAssertEqual(numberOfRows,
menuItemsList.count,
"Returns two rows as we're passing two menu items")
}
Return menuItems
‘s actual count instead of a hard-coded value of 1
to make the above test pass.
func tableView(tableView: UITableView!,
numberOfRowsInSection section: Int)
-> Int
{
return menuItems!.count
}
We also need to make sure that the data source returns correct number of sections. Here is a test for that:
func testReturnsOnlyOneSection() {
let dataSource = MenuTableDefaultDataSource()
let numberOfSections = dataSource.numberOfSectionsInTableView(nil)
XCTAssertEqual(numberOfSections, 1,
"There should only be one section")
}
Return 1
from numberOfSectionsInTableView
method to make previous test pass. Although this method is not required by UITableViewDataSource
protocol and the default implementation already returns 1
, we need to implement it in order to be able to call it from the test.
func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
return 1
}
One other test I would have liked to write is to verify that the data source throws an exception if asked for number of rows in a section whose index is anything other than 0. However, I couldn’t find our old friend
XCTAssertThrows
in Xcode 6 version of XCTest. I don’t know how else to verify that an exception was thrown.
Testing every aspect of a view could turn out to be tedius on iOS. I tend to test at least the semantics behind a view. By that I mean test what a view should represent. In this case, each table view cell represents a menu item. Therefore, I would at least like to verify that a cell displays the title from a respective menu item. Here is what that test looks like:
func testEachCellContainsTitleForRespectiveMenuItem() {
let testMenuItem = MenuItem(title: "Test menu item")
let dataSource = MenuTableDefaultDataSource()
dataSource.setMenuItems([testMenuItem])
let firstMenuItem = NSIndexPath(forRow: 0, inSection: 0)
let cell =
dataSource.tableView(nil, cellForRowAtIndexPath: firstMenuItem)
XCTAssertEqual(cell.textLabel.text!,
"Test menu item",
"A cell contains the title of a menu item it's representing")
}
Following changes to tableView:cellForRowAtIndexPath:
method should make the previous test pass.
func tableView(tableView: UITableView!,
cellForRowAtIndexPath indexPath: NSIndexPath!)
-> UITableViewCell!
{
// Ideally we should be reusing table view cells here
let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)
let menuItem = menuItems?[indexPath.row]
cell.textLabel.text = menuItem?.title
cell.detailTextLabel.text = menuItem?.subTitle
cell.imageView.image = UIImage(named: menuItem?.iconName)
cell.accessoryType = .DisclosureIndicator
return cell
}
Let’s refactor the tests for MenuTableDefaultDataSource
we have written so far by extracting the common code into setup
method.
import UIKit
import XCTest
class MenuTableDefaultDataSourceTests: XCTestCase {
var dataSource: MenuTableDefaultDataSource?
var menuItemsList: [MenuItem]?
override func setUp() {
super.setUp()
let testMenuItem = MenuItem(title: "Test menu item")
menuItemsList = [testMenuItem]
dataSource = MenuTableDefaultDataSource()
dataSource!.setMenuItems(menuItemsList!)
}
func testReturnsOneRowForOneMenuItem() {
let numberOfRows =
dataSource!.tableView(nil, numberOfRowsInSection:0)
XCTAssertEqual(numberOfRows,
menuItemsList!.count,
"Only one row is returned since we're passing one menu item")
}
func testReturnsTwoRowsForTwoMenuItems() {
let testMenuItem1 = MenuItem(title: "Test menu item 1")
let testMenuItem2 = MenuItem(title: "Test menu item 2")
let menuItemsList = [testMenuItem1, testMenuItem2]
dataSource!.setMenuItems(menuItemsList)
let numberOfRows =
dataSource!.tableView(nil, numberOfRowsInSection:0)
XCTAssertEqual(numberOfRows,
menuItemsList.count,
"Returns two rows as we're passing two menu items")
}
func testReturnsOnlyOneSection() {
let numberOfSections =
dataSource!.numberOfSectionsInTableView(nil)
XCTAssertEqual(numberOfSections, 1,
"There should only be one section")
}
func testEachCellContainsTitleForRespectiveMenuItem() {
let firstMenuItem = NSIndexPath(forRow: 0, inSection: 0)
let cell =
dataSource!.tableView(nil,
cellForRowAtIndexPath: firstMenuItem)
XCTAssertEqual(cell.textLabel.text!,
"Test menu item",
"A cell contains the title of a menu item it's representing")
}
}
Handling Menu Item Tap Event #
As it turns out our table view setup is fairly simple. Therefore, it makes sense to use the same object as a data source and a delegate. When a table view cell is tapped, the data source will post a notification. MenuViewController
(or any other class that is interested in that notification) can then query the notification to find out which cell was tapped and take appropriate action.
This design is somewhat inspired by chapter 9 from Test-Driven iOS Development book. Let’s add the delegate related details to MenuTableDataSource
protocol.
import UIKit
let MenuTableDataSourceDidSelectItemNotification =
"MenuTableDataSourceDidSelectItemNotification"
protocol MenuTableDataSource : UITableViewDataSource, UITableViewDelegate {
func setMenuItems(menuItems: [MenuItem])
}
Now we need to verify that the data source indeed posts a notification when a menu item is tapped. Following tests will do that.
class MenuTableDefaultDataSourceTests: XCTestCase {
var dataSource: MenuTableDefaultDataSource?
var testMenuItem: MenuItem?
var menuItemsList: [MenuItem]?
var postedNotification: NSNotification?
var selectedIndexPath: NSIndexPath?
override func setUp() {
super.setUp()
testMenuItem = MenuItem(title: "Test menu item")
menuItemsList = [testMenuItem!]
selectedIndexPath = NSIndexPath(forRow: 0, inSection: 0)
dataSource = MenuTableDefaultDataSource()
dataSource!.setMenuItems(menuItemsList!)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self,
selector: "didReceiveNotification:",
name: MenuTableDataSourceDidSelectItemNotification,
object: nil)
}
override func tearDown() {
super.tearDown()
postedNotification = nil
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func didReceiveNotification(notification: NSNotification) {
postedNotification = notification
}
func testANotificationIsPostedWhenACellIsTapped() {
dataSource!.tableView(nil,
didSelectRowAtIndexPath:selectedIndexPath)
XCTAssertEqual(postedNotification!.name,
MenuTableDataSourceDidSelectItemNotification,
"Data source posts a notification when a cell is tapped")
}
func testPostedNotificationContainsMenuItemInfo() {
dataSource!.tableView(nil,
didSelectRowAtIndexPath:selectedIndexPath)
XCTAssertTrue(postedNotification!.object.isEqual(testMenuItem!),
"Notification contains menu item object")
}
// Previous tests for data source are excluded here...
}
In setup
method, we added the test class to be an observer for a notification with name MenuTableDataSourceDidSelectItemNotification
. When that notification arrives, didReceiveNotification:
method should get called. The notification object passed to that method is stored in postedNotification
variable. Then we verify that it has the correct name and menu item instance. It is important that the test class is removed as an observer in tearDown
method. We go through this complex process to verify that a notification is indeed posted because NSNotificationCenter doesn’t provide an API to query if a notification has been posted to it.
In Building Menu Items section I recommended that we use fake objects in tests. However, I am using
NSNotificationCenter
class straight up in above tests. Generally, I don’t use stand-ins for objects provided by Apple frameworks. They are fairly reliable in terms of stability and speed. That being said, if it turns out that the reliability of your tests are going down due to the use of real objects provided by Apple frameworks, don’t hesitate to create test replacements for them.
Implement tableView:didSelectRowAtIndexPath:
method from UITableViewDataSource
protocol in MenuTableDefaultDataSource
class to make above tests pass.
func tableView(tableView: UITableView!,
didSelectRowAtIndexPath indexPath: NSIndexPath!)
{
let menuItem = menuItems?[indexPath.row]
let notification =
NSNotification(name: MenuTableDataSourceDidSelectItemNotification,
object:menuItem)
NSNotificationCenter.defaultCenter().postNotification(notification)
}
Managing Menu Table View #
MenuViewController
will be responsible for managing the table view and any other views that might need to be createed in order to present the menu. The first thing we need to make sure is that we can give it a data source. We also need to ensure that it has a title and a table view. Create a new test file named MenuViewControllerTests.swift
and replace its content with following tests.
import UIKit
import XCTest
class MenuViewControllerTests: XCTestCase {
var menuViewController: MenuViewController?
var dataSource: MenuTableDataSource?
var tableView: UITableView?
override func setUp() {
super.setUp()
dataSource = MenuTableFakeDataSource()
tableView = UITableView()
menuViewController = MenuViewController()
menuViewController?.dataSource = dataSource
menuViewController?.tableView = tableView
}
func testHasATitle() {
menuViewController?.viewDidLoad()
XCTAssertEqual(menuViewController!.title!, "App Menu",
"Menu view should show a proper title")
}
func testCanBeAssignedADataSource() {
XCTAssertTrue(dataSource!.isEqual(menuViewController?.dataSource),
"A data source can be assigned to a menu view controller")
}
func testHasATableView() {
XCTAssertTrue(tableView!.isEqual(menuViewController?.tableView),
"Menu view controller has a table view")
}
}
Instead of using a real data source object here, we are using a fake one named MenuTableFakeDataSource
. Create a new Swift file named MenuTableFakeDataSource.swift
in AppMenuTests
target and replace its content with following.
import Foundation
import UIKit
class MenuTableFakeDataSource : NSObject, MenuTableDataSource {
func setMenuItems(menuItems: [MenuItem]) {
}
// MARK: - UITableView data source methods
func tableView(tableView: UITableView!,
numberOfRowsInSection section: Int)
-> Int
{
return 1
}
func tableView(tableView: UITableView!,
cellForRowAtIndexPath indexPath: NSIndexPath!)
-> UITableViewCell!
{
return nil
}
}
All MenuTableFakeDataSource
does is provide stub implementation of all the required methods in MenuTableDataSource
protocol so that it can stand in for any object that conforms to MenuTableDataSource
. Now create MenuViewController
class (Right click AppMenu group > New File > iOS > Source > Cocoa Touch Class). Make it a subclass of UIViewController
and select the Also create XIB file checkbox. Don’t forget to add it to both targets. The tests above should pass just by declaring two properties and assigning a title.
import UIKit
class MenuViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var dataSource: MenuTableDataSource?
override func viewDidLoad() {
super.viewDidLoad()
title = "App Menu"
}
}
Change the size of main view in MenuViewController.xib
to iPhone 4-inch from Simulated Metrics section in Attributes Inspector. Set the view’s orientation to Portrait. After that add a table view as the main view’s subview. Connect the table view in XIB to the tableView
outlet in MenuViewController
class.
Next we need to make sure that MenuViewController
sets the table view’s delegate and dataSource properties to the data source object we assigned to it. viewDidLoad method is where we want it to make that connection. Following tests should ensure that.
func testTableViewIsGivenADataSourceInViewDidLoad() {
menuViewController?.viewDidLoad()
XCTAssertTrue(tableView!.dataSource.isEqual(dataSource),
"Data source for the menu table view is set in viewDidLoad method")
}
func testTableViewIsGivenADelegateInViewDidLoad() {
menuViewController?.viewDidLoad()
XCTAssertTrue(tableView!.delegate.isEqual(dataSource),
"Delegate for the menu table view is set in viewDidLoad method")
}
Set table view’s data source and delegate properties in viewDidLoad
to make above tests pass.
override func viewDidLoad() {
super.viewDidLoad()
title = "App Menu"
tableView.dataSource = dataSource
tableView.delegate = dataSource
}
In Handling Menu Item Tap Event we made MenuTableDefaultDataSource
post a notification when a menu item is tapped. MenuViewController
needs to listen to that notification in order to show a correct view for that menu item. If that notification arrives when MenuViewController
’s view is hidden, it should ignore it. Therefore, it should register for that notification in viewDidAppear:
method. It should also stop listening for that notification in viewDidDisappear:
method. Let’s capture that requirement through tests.
let postedNotification = "MenuViewControllerTestsPostedNotification"
class MenuViewControllerTests: XCTestCase {
var menuViewController: MenuViewController?
var dataSource: MenuTableDataSource?
var tableView: UITableView?
override func setUp() {
super.setUp()
dataSource = MenuTableFakeDataSource()
tableView = UITableView()
menuViewController = MenuViewController()
menuViewController?.dataSource = dataSource
menuViewController?.tableView = tableView
}
override func tearDown() {
super.tearDown()
objc_removeAssociatedObjects(menuViewController)
}
// ...
func testRegistrationForNotificationHappensInViewDidAppear() {
swizzleNotificationHandler()
menuViewController?.viewDidAppear(false)
let notification =
NSNotification(
name: MenuTableDataSourceDidSelectItemNotification,
object: nil)
NSNotificationCenter.defaultCenter().postNotification(notification)
XCTAssertNotNil(
objc_getAssociatedObject(menuViewController, postedNotification),
"Listens to notification only when it's view is visible")
}
func testRemovesItselfAsListenerForNotificationInViewDidDisappear() {
swizzleNotificationHandler()
menuViewController?.viewDidAppear(false)
menuViewController?.viewDidDisappear(false)
let notification =
NSNotification(
name: MenuTableDataSourceDidSelectItemNotification,
object: nil)
NSNotificationCenter.defaultCenter().postNotification(notification)
XCTAssertNil(
objc_getAssociatedObject(menuViewController, postedNotification),
"Stops listening for notfication when view is not visible anymore")
}
// Mark: - Method swizzling
func swizzleNotificationHandler() {
var realMethod: Method =
class_getInstanceMethod(
object_getClass(menuViewController),
Selector.convertFromStringLiteral(
"didSelectMenuItemNotification:"))
var testMethod: Method =
class_getInstanceMethod(
object_getClass(menuViewController),
Selector.convertFromStringLiteral(
"testImpl_didSelectMenuItemNotification:"))
method_exchangeImplementations(realMethod, testMethod)
}
}
extension MenuViewController {
func testImpl_didSelectMenuItemNotification(
notification: NSNotification)
{
objc_setAssociatedObject(self,
postedNotification,
notification,
UInt(OBJC_ASSOCIATION_RETAIN))
}
}
That is a lot of code. Let me explain. In order to verify that MenuViewController
registers itself to listen for MenuTableDataSourceDidSelectItemNotification
, we need to somehow get hold of the method that gets called when that notification arrives. Once we get hold of it, we need to capture the notification passed to that method and verify its existence. We could simply make a non-private property for that notification in MenuViewController
, but I don’t like that approach. MenuViewController
shouldn’t be forced to expose something just because tests need it. There has to be a better way. How about we swizzle the notification handler during runtime by providing a different implementation that is suited for our testing purpose? Following code will do just that.
func swizzleNotificationHandler() {
var realMethod: Method =
class_getInstanceMethod(
object_getClass(menuViewController),
Selector.convertFromStringLiteral(
"didSelectMenuItemNotification:"))
var testMethod: Method =
class_getInstanceMethod(
object_getClass(menuViewController),
Selector.convertFromStringLiteral(
"testImpl_didSelectMenuItemNotification:"))
method_exchangeImplementations(realMethod, testMethod)
}
And here is the extension for MenuViewController
class that provides the test implementation:
extension MenuViewController {
func testImpl_didSelectMenuItemNotification(
notification: NSNotification)
{
objc_setAssociatedObject(self,
postedNotification,
notification,
UInt(OBJC_ASSOCIATION_RETAIN))
}
}
All we are doing here is assign the notification to postedNotification
constant so that we can evaluate it in tests. In testRegistrationForMenuItemTappedNotificationHappensInViewDidAppear
after we swizzle the notification handler, we call viewDidAppear
, post a notification and verify that postedNotification
is not nil. Whereas in testRemovesItselfAsListenerForMenuItemTappedNotificationInViewDidDisappear
, we first call viewDidAppear
so that MenuViewController
registers for the notification. After that we call viewDidDisappear
and post a notification. This notification shouldn’t reach to MenuViewController
as we expect it to remove itself as an observer from NSNotificationCenter
in viewDidDisapper
method.
To make the tests pass, all we need to do is register and unregister for the notification in proper places.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "didSelectMenuItemNotification:",
name: MenuTableDataSourceDidSelectItemNotification,
object: nil)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self,
name: MenuTableDataSourceDidSelectItemNotification,
object: nil)
}
func didSelectMenuItemNotification(notification: NSNotification?) {
// Handle notification
}
Sliding Views In #
When a menu item is tapped, we need to display a view. But which one? How about we ask the menu item itself? To keep things simple, let’s store the view controller’s name in tapHandlerName
property in MenuItem
.
class MenuItemTests: XCTestCase {
// ...
func testThatMenuItemCanBeAssignedATapHandlerName() {
menuItem!.tapHandlerName = "someViewController"
XCTAssertEqual(menuItem!.tapHandlerName!,
"someViewController",
"Tap handler name should be what we assigned")
}
}
class MenuItem {
// ...
var tapHandlerName: String?
}
It’s perfectly fine for a menu item to not have a tap handler. Therefore, we should make the tapHandlerName
property an optional. Now that we have added an additional property to MenuItem
, we need to adjust menuItems.plist
, FakeMenuItemsReader
, MenuItemsPlistReaderTests
, MenuItemBuilderTests
, and MenuItemBuilder
. The adjusted code is listed below.
<!--menuItems.plist-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
//...
<key>tapHandlerName</key>
<string>ContributionsViewController</string>
</dict>
<dict>
//...
<key>tapHandlerName</key>
<string>RepositoriesViewController</string>
</dict>
<dict>
//...
<key>tapHandlerName</key>
<string>PublicActivityViewController</string>
</dict>
</array>
</plist>
class FakeMenuItemsReader : MenuItemsReader {
// ...
func readMenuItems() -> ([[String : String]]?, NSError?) {
let menuItem1 =
missingTitle ? menuItem1WithMissingTitle()
: menuItem1WithNoMissingTitle()
let menuItem2 = ["title": "Menu Item 2",
"subTitle": "Menu Item 2 subtitle",
"iconName": "iconName2",
"tapHandlerName": "someViewController1"]
return ([menuItem1, menuItem2], nil)
}
func menuItem1WithMissingTitle() -> [String : String] {
return ["subTitle": "Menu Item 1 subtitle",
"iconName": "iconName1",
"tapHandlerName": "someViewController2"]
}
// ...
}
class MenuItemsPlistReaderTests: XCTestCase {
// ...
func testPlistIsDeserializedCorrectly() {
// ...
XCTAssertEqual(firstRow!["tapHandlerName"]!,
"ContributionsViewController",
"1st row's tap handler should be what's in plist")
// ...
XCTAssertEqual(secondRow!["tapHandlerName"]!,
"RepositoriesViewController",
"2nd row's tap handler should be what's in plist")
// ...
XCTAssertEqual(thirdRow!["tapHandlerName"]!,
"PublicActivityViewController",
"3rd row's tap handler should be what's in plist")
}
}
class MenuItemBuilderTests: XCTestCase {
// ...
func testMenuItemPropertiesContainValuesPresentInDictionary() {
// ...
let rawDictionary1 = metadata![0]
let menuItem1 = menuItems![0]
// ...
XCTAssertEqual(menuItem1.tapHandlerName!,
rawDictionary1["tapHandlerName"]!,
"1st menu item's tap handler should be what's in the 1st dict")
let rawDictionary2 = metadata![1]
let menuItem2 = menuItems![1]
// ...
XCTAssertEqual(menuItem2.tapHandlerName!,
rawDictionary2["tapHandlerName"]!,
"2nd menu item's tap handler should be what's in the 2nd dict")
}
}
class MenuItemBuilder {
func buildMenuItemsFromMetadata(metadata: [[String : String]])
-> ([MenuItem]?, NSError?)
{
// ...
for dictionary in metadata {
if let title = dictionary["title"] {
let menuItem = MenuItem(title: title)
menuItem.subTitle = dictionary["subTitle"]
menuItem.iconName = dictionary["iconName"]
menuItem.tapHandlerName = dictionary["tapHandlerName"]
menuItems.append(menuItem)
}
else {
error = missingTitleError()
menuItems.removeAll(keepCapacity: false)
break
}
}
return (menuItems, error)
}
// ...
}
Next up we need to make sure that MenuViewController
displays the correct view when a menu item is tapped. Following tests will do just that.
class MenuViewControllerTests: XCTestCase {
// ...
var navController: UINavigationController?
override func setUp() {
// ...
navController =
UINavigationController(rootViewController: menuViewController)
}
// ...
func testCorrectViewIsDisplayedWhenContributionsMenuItemIsTapped() {
let menuItem = MenuItem(title: "Contributions")
menuItem.tapHandlerName = "ContributionsViewController"
let notification =
NSNotification(
name: MenuTableDataSourceDidSelectItemNotification,
object: menuItem)
menuViewController?.didSelectMenuItemNotification(notification)
let topViewController = navController?.topViewController
XCTAssertTrue(topViewController is ContributionsViewController,
"Contributions view is displayed for Contributions menu item")
}
func testCorrectViewIsDisplayedWhenRepositoriesMenuItemIsTapped() {
let menuItem = MenuItem(title: "Repositories")
menuItem.tapHandlerName = "RepositoriesViewController"
let notification =
NSNotification(
name: MenuTableDataSourceDidSelectItemNotification,
object: menuItem)
menuViewController?.didSelectMenuItemNotification(notification)
let topViewController = navController?.topViewController
XCTAssertTrue(topViewController is RepositoriesViewController,
"Repositories view is displayed for Contributions menu item")
}
func testCorrectViewIsDisplayedWhenPublicActivityMenuItemIsTapped() {
let menuItem = MenuItem(title: "PublicActivity")
menuItem.tapHandlerName = "PublicActivityViewController"
let notification =
NSNotification(
name: MenuTableDataSourceDidSelectItemNotification,
object: menuItem)
menuViewController?.didSelectMenuItemNotification(notification)
let topViewController = navController?.topViewController
XCTAssertTrue(topViewController is PublicActivityViewController,
"Public activity view is displayed for Contributions menu item")
}
// ...
}
Let’s make MenuViewController
push the right view controller into app navigation stack.
class MenuViewController: UIViewController {
// ...
func didSelectMenuItemNotification(notification: NSNotification?) {
var menuItem: MenuItem? = notification!.object as? MenuItem
if menuItem != nil {
var tapHandler: UIViewController?
switch menuItem!.tapHandlerName! {
case "ContributionsViewController":
tapHandler =
ContributionsViewController(
nibName: "ContributionsViewController",
bundle: nil)
case "RepositoriesViewController":
tapHandler =
RepositoriesViewController(
nibName: "RepositoriesViewController",
bundle: nil)
case "PublicActivityViewController":
tapHandler =
PublicActivityViewController(
nibName: "PublicActivityViewController",
bundle: nil)
default:
tapHandler = nil
}
if tapHandler != nil {
self.navigationController.pushViewController(tapHandler,
animated: true)
}
}
}
}
We also need to create tap handler classes. Create following view controllers and add them to both AppMenu and AppMenuTests targets. Don’t forget to also create a XIB file for each one of them.
ContributionsViewController
RepositoriesViewController
PublicActivityViewController
You might be wondering why we didn’t create view controllers listed above during runtime instead of using the switch case statement. Two reasons:
- I am not quite sure what’s the best way to do that in Swift. In Objective-C you could easily accomplish that with following code.
UIViewController *tapHandler = nil;
Class tapHandlerClass =
NSClassFromString(menuItem.tapHandlerName)
if (tapHandlerClass) {
tapHandler = [[tapHandlerClass alloc] init];
}
- Unlike Objective-C, Swift requires you to specify which XIB file to use when you create an instance of a view controller even though the XIB name is the same as the view controller’s class name. Therefore, simply calling the
alloc
,init
equivalent in Swift isn’t sufficient.
Unfortunately, that still doesn’t make the tests pass. It turns out that so far we have built every class we initially set out to build except AppMenuManager
. Once that class is built, we should be in a position to make above tests pass. Let’s get to it.
Managing App Menu #
Create a new test file named AppMenuManagerTests.swift
in AppMenuTests target. Add following tests to it.
import UIKit
import XCTest
class AppMenuManagerTests: XCTestCase {
var menuManager: AppMenuManager?
var fakeMenuItemsReader: FakeMenuItemsReader?
var fakeMenuItemBuilder: FakeMenuItemBuilder?
var menuViewController: MenuViewController?
override func setUp() {
super.setUp()
menuManager = AppMenuManager()
fakeMenuItemsReader = FakeMenuItemsReader()
fakeMenuItemBuilder = FakeMenuItemBuilder()
menuManager?.menuItemsReader = fakeMenuItemsReader
menuManager?.menuItemBuilder = fakeMenuItemBuilder
}
func testReturnsNilIfMetadataCouldNotBeRead() {
fakeMenuItemsReader?.errorToReturn = fakeError()
menuViewController = menuManager?.menuViewController()
XCTAssertNil(menuViewController,
"Doesn't create menu VC if metadata couldn't be read")
}
func testMetadataIsPassedToMenuItemBuilder() {
menuViewController = menuManager?.menuViewController()
var (metadataReturnedByReader, _) =
fakeMenuItemsReader!.readMenuItems()
let metadataReceivedByBuilder =
fakeMenuItemBuilder!.metadata
XCTAssertTrue(metadataReceivedByBuilder?.count ==
metadataReturnedByReader?.count,
"Number of dictionaries in metadata should match")
}
func testReturnsNilIfMenuItemsCouldNotBeBuilt() {
fakeMenuItemBuilder?.errorToReturn = fakeError()
menuViewController = menuManager?.menuViewController()
XCTAssertNil(menuViewController,
"Doesn't create menu VC if menu items couldn't be built")
}
func testCreatesMenuViewControllerIfMenuItemsAvailable() {
fakeMenuItemBuilder?.menuItemsToReturn = fakeMenuItems()
menuViewController = menuManager?.menuViewController()
XCTAssertNotNil(menuViewController,
"Creates menu view controller if menu items are available")
XCTAssertNotNil(menuViewController?.dataSource,
"Menu view controller is given a data source")
}
func fakeError() -> NSError {
let errorMessage = "Fake error description"
let userInfo = [NSLocalizedDescriptionKey: errorMessage]
return NSError(domain: "Fake Error domain",
code: 0,
userInfo: userInfo)
}
func fakeMenuItems() -> [MenuItem] {
let menuItem = MenuItem(title: "Fake menu item")
return [menuItem]
}
}
AppMenuManager
is responsible for creating MenuViewController
if MenuItem
objects were created successfully from the metadata. If not, it just returns nil. Since AppMenuManager
mostly coordinates the interaction between various objects rather than doing the work itself, we also need to make sure that it passes the metadata (if read successfully) to the builder. You might have noticed that we are using fake menu items reader and builder objects here so that we can control what gets returned to app menu manager in tests. We built a fake menu items reader in Building Menu Items, but it doesn’t provide a way for us to set the error. Let’s take care of that.
class FakeMenuItemsReader : MenuItemsReader {
var missingTitle: Bool = false
var errorToReturn: NSError? = nil
func readMenuItems() -> ([[String : String]]?, NSError?) {
if errorToReturn != nil {
return (nil, errorToReturn)
}
else {
let menuItem1 =
missingTitle ? menuItem1WithMissingTitle()
: menuItem1WithNoMissingTitle()
let menuItem2 = ["title": "Menu Item 2",
"subTitle": "Menu Item 2 subtitle",
"iconName": "iconName2",
"tapHandlerName": "someViewController1"]
return ([menuItem1, menuItem2], nil)
}
}
// ...
Next we need to create FakeMenuItemBuilder
class. Now that there is going to be more than one class playing the role of a menu item builder, we should create a protocol to make it clear what it means for a class to become a menu item builder. For now, playing that role means implementing buildMenuItemsFromMetadata
method correctly. Listed below is the new protocol.
import Foundation
protocol MenuItemBuilder {
func buildMenuItemsFromMetadata(metadata: [[String : String]]) -> ([MenuItem]?, NSError?)
}
Wait a minute. Didn’t we already name our real builder class MenuItemBuilder
? Yes we did. MenuItemBuilder
name is better suited for a protocol. Let’s rename the original builder class to MenuItemDefaultBuilder
.
import Foundation
let MenuItemDefaultBuilderErrorDomain = "MenuItemDefaultBuilderErrorDomain"
enum MenuItemDefaultBuilderErrorCode : Int {
case MissingTitle
}
class MenuItemDefaultBuilder : MenuItemBuilder {
func buildMenuItemsFromMetadata(metadata: [[String : String]])
-> ([MenuItem]?, NSError?)
{
var menuItems = [MenuItem]()
var error: NSError?
for dictionary in metadata {
if let title = dictionary["title"] {
let menuItem = MenuItem(title: title)
menuItem.subTitle = dictionary["subTitle"]
menuItem.iconName = dictionary["iconName"]
menuItem.tapHandlerName = dictionary["tapHandlerName"]
menuItems.append(menuItem)
}
else {
error = missingTitleError()
menuItems.removeAll(keepCapacity: false)
break
}
}
return (menuItems, error)
}
private func missingTitleError() -> NSError {
let userInfo =
[NSLocalizedDescriptionKey: "All menu items must have a title"]
return NSError(domain: MenuItemDefaultBuilderErrorDomain,
code: MenuItemDefaultBuilderErrorCode.MissingTitle.toRaw(),
userInfo: userInfo)
}
}
We also need to adjust tests to use the new name.
class MenuItemDefaultBuilderTests: XCTestCase {
var menuItemBuilder: MenuItemDefaultBuilder?
var fakeMenuItemsReader: FakeMenuItemsReader?
var menuItems: [MenuItem]?
var error: NSError?
override func setUp() {
fakeMenuItemsReader = FakeMenuItemsReader()
fakeMenuItemsReader!.missingTitle = true
let (metadata, _) =
fakeMenuItemsReader!.readMenuItems()
menuItemBuilder = MenuItemDefaultBuilder()
(menuItems, error) =
menuItemBuilder!.buildMenuItemsFromMetadata(metadata!)
}
func testCorrectErrorDomainIsReturnedWhenTitleIsMissing() {
let errorDomain = error?.domain
XCTAssertEqual(errorDomain!,
MenuItemDefaultBuilderErrorDomain,
"Correct error domain is returned")
}
func testMissingTitleErrorCodeIsReturnedWhenTitleIsMissing() {
let errorCode = error?.code
XCTAssertEqual(errorCode!,
MenuItemDefaultBuilderErrorCode.MissingTitle.toRaw(),
"Correct error code is returned")
}
// ...
}
Finally, here is what the FakeMenuItemReader
class looks like. You don’t need to add this class to the AppMenu target since it’s only used in tests.
import Foundation
class FakeMenuItemBuilder : MenuItemBuilder {
var errorToReturn: NSError? = nil
var menuItemsToReturn: [MenuItem]? = nil
var metadata: [[String : String]]? = nil
func buildMenuItemsFromMetadata(metadata: [[String : String]])
-> ([MenuItem]?, NSError?)
{
self.metadata = metadata
return (menuItemsToReturn, errorToReturn)
}
}
It makes the metadata passed to it available for inspection. It also allows us to set the error and menu items we want it to return which is very convenient. Now we are ready to build the AppMenuManager
class. Here is what it looks like.
import Foundation
import UIKit
class AppMenuManager {
var menuItemsReader: MenuItemsReader? = nil
var menuItemBuilder: MenuItemBuilder? = nil
func menuViewController() -> MenuViewController? {
let (metadata, metadataError) =
menuItemsReader!.readMenuItems()
if metadataError != nil {
tellUserAboutError(metadataError!)
}
else if let menuItems = menuItemsFromMetadata(metadata!) {
return menuViewControllerFromMenuItems(menuItems)
}
return nil
}
private func tellUserAboutError(error: NSError) {
println("Error domain: \(error.domain)")
println("Error code: \(error.code)")
let alert = UIAlertView(title: "Error",
message: error.localizedDescription,
delegate: nil,
cancelButtonTitle: nil,
otherButtonTitles: "OK")
alert.show()
}
private func menuItemsFromMetadata(metadata: [[String : String]])
-> [MenuItem]?
{
let (menuItems, builderError) =
menuItemBuilder!.buildMenuItemsFromMetadata(metadata)
if builderError != nil {
tellUserAboutError(builderError!)
return nil
}
return menuItems
}
private func menuViewControllerFromMenuItems(menuItems: [MenuItem])
-> MenuViewController
{
let dataSource = MenuTableDefaultDataSource()
dataSource.menuItems = menuItems
let menuViewController =
MenuViewController(nibName: "MenuViewController", bundle: nil)
menuViewController.dataSource = dataSource
return menuViewController
}
}
I apologize for not staying true to the read-green-refactor cycle here. I wanted to focus more on important techniques that make writting tests a bit easier rather than showing you every single step in the process. One of those techniques is creating fake (or test double) objects that play the same role as the real objects so that we can easily swap them to make our tests more maintainable. Speaking of fake objects, Martin Fowler has written a great post on the topic.
Before we move on, I would like to emphasize the importance of Dependency Injection in writing testable and reusable classes. Our AppMenuManager
class needs to work with two other classes that conform to MenuItemsReader
and MenuItemBuilder
protocols to successfully create MenuItem
objects. Had we not exposed these two dependencies via public properties, we would not have been able to pass in fake objects. Those fake objects came very handy while setting up the desired test scenarios in order to verify that AppMenuManager
behaved as expected. Therefore, I recommend exposing every single dependency your classes have unless those dependencies are classes provided by Apple frameworks.
Putting It All Together #
We are almost there. Now that we have built every class, let’s put them together in AppDelegate
. But first we will write some tests to verify that AppDeleate
behaves as expected. Create a new test file named AppDelegateTests.swift
in AppMenuTests target. Add following tests to it.
import UIKit
import XCTest
class AppDelegateTests: XCTestCase {
var window: UIWindow?
var navController: UINavigationController?
var appDelegate: AppDelegate?
var appMenuManager: AppMenuManager?
var didFinishLaunchingWithOptionsReturnValue: Bool?
override func setUp() {
super.setUp()
window = UIWindow()
navController = UINavigationController()
appMenuManager = AppMenuManager()
appDelegate = AppDelegate()
appDelegate?.window = window
appDelegate?.navController = navController
}
func testRootVCForWindowIsNotSetIfMenuViewControllerCannotBeCreated() {
class FakeAppMenuManager: AppMenuManager {
override func menuViewController() -> MenuViewController? {
return nil
}
}
appDelegate?.appMenuManager = FakeAppMenuManager()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
XCTAssertNil(window!.rootViewController,
"Window's root VC shouldn't be set if menu VC can't be created")
}
func testWindowHasRootViewControllerIfMenuViewControllerIsCreated() {
class FakeAppMenuManager: AppMenuManager {
override func menuViewController() -> MenuViewController? {
return MenuViewController()
}
}
appDelegate?.appMenuManager = FakeAppMenuManager()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(window!.rootViewController, navController!,
"App delegate's nav controller should be the root view controller")
}
func testMenuViewControllerIsRootVCForNavigationController() {
class FakeAppMenuManager: AppMenuManager {
override func menuViewController() -> MenuViewController? {
return MenuViewController()
}
}
appDelegate?.appMenuManager = FakeAppMenuManager()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
let topViewController =
appDelegate?.navController?.topViewController
XCTAssertTrue(topViewController is MenuViewController,
"Menu view controlelr is root VC for nav controller")
}
func testWindowIsKeyAfterAppIsLaunched() {
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
XCTAssertTrue(window!.keyWindow,
"App delegate's window should be the key window for the app")
}
func testAppDidFinishLaunchingDelegateMethodAlwaysReturnsTrue() {
didFinishLaunchingWithOptionsReturnValue =
appDelegate?.application(nil,
didFinishLaunchingWithOptions: nil)
XCTAssertTrue(didFinishLaunchingWithOptionsReturnValue!,
"Did finish launching delegate method should return true")
}
}
In Managing App Menu, we created
MenuItemBuilder
protocol when we realized that we needed a fake object that could stand-in for the real menu builder. But, here we are creating fake app menu manager objects inside the tests themselves. It’s perfectly fine to do so. If we decide to renamemenuViewController
method in real app menu manager class, Swift will force us to modify all our fake objects to use the new method name. Because of that, all these fake objects will always be in sync with the real app menu manager. This approach comes very handy if you need to create quick fake objects inside the tests.
When we created a new Xcode project, AppDelegate
was added only to the AppMenu target. We need to add it to `AppMenuTests* target as well. After that replace its content with following:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navController: UINavigationController?
var appMenuManager: AppMenuManager?
func application(application: UIApplication!,
didFinishLaunchingWithOptions launchOptions: NSDictionary!)
-> Bool
{
if window == nil {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
}
let menuItemsPlistReader = MenuItemsPlistReader()
menuItemsPlistReader.plistToReadFrom = "menuItems"
if appMenuManager == nil {
appMenuManager = AppMenuManager()
}
appMenuManager!.menuItemsReader = menuItemsPlistReader
appMenuManager!.menuItemBuilder = MenuItemDefaultBuilder()
if let menuViewController = appMenuManager!.menuViewController() {
if navController == nil {
navController = UINavigationController()
}
navController?.viewControllers = [menuViewController]
window!.rootViewController = navController!
}
window!.makeKeyAndVisible()
return true
}
}
It would be nice to extract the code that configures AppMenuManager
out from AppDelegate
. We are going to apply what Graham Lee taught us in Test-Driven iOS Development here and create our own dependency injection class instead of using a full blown depdendency injection framework. AppMenu is a simple app, at least for now. So we shouldn’t be adding dependencies to it unless we need them. Create a new test file named ObjectConfiguratorTests.swift
in AppMenuTests target and replace its content with following.
import UIKit
import XCTest
class ObjectConfiguratorTests: XCTestCase {
var objectConfigurator: ObjectConfigurator?
override func setUp() {
super.setUp()
objectConfigurator = ObjectConfigurator()
}
func testConfiguresAppMenuManagerCorrectly() {
let appMenuManager = objectConfigurator?.appMenuManager()
XCTAssertNotNil(appMenuManager, "App menu manager is not nil")
XCTAssertTrue(appMenuManager?.menuItemsReader != nil,
"App menu manager has a menu items reader")
XCTAssertTrue(appMenuManager?.menuItemBuilder != nil,
"App menu manager has a menu item builder")
}
}
Create the ObjectConfigurator
class and add it to both targets. Replace its content with following.
import UIKit
class ObjectConfigurator {
func appMenuManager() -> AppMenuManager {
let appMenuManager = AppMenuManager()
let menuItemsPlistReader = MenuItemsPlistReader()
menuItemsPlistReader.plistToReadFrom = "menuItems"
appMenuManager.menuItemsReader = menuItemsPlistReader
appMenuManager.menuItemBuilder = MenuItemDefaultBuilder()
return appMenuManager
}
}
Instead of creating an AppMenuManager
object itself, app delegate will tell the object configurator to do so. Let’s make changes to AppDelegate
and its tests to include the new approach.
class FakeAppMenuManager: AppMenuManager {
override func menuViewController() -> MenuViewController? {
return MenuViewController()
}
}
class FakeObjectConfigurator : ObjectConfigurator {
override func appMenuManager() -> AppMenuManager {
return FakeAppMenuManager()
}
}
class AppDelegateTests: XCTestCase {
var window: UIWindow?
var navController: UINavigationController?
var appDelegate: AppDelegate?
var objectConfigurator: ObjectConfigurator?
var didFinishLaunchingWithOptionsReturnValue: Bool?
override func setUp() {
super.setUp()
window = UIWindow()
navController = UINavigationController()
appDelegate = AppDelegate()
appDelegate?.window = window
appDelegate?.navController = navController
}
func testRootVCForWindowIsNotSetIfMenuViewControllerCannotBeCreated() {
class FakeAppMenuManager: AppMenuManager {
override func menuViewController() -> MenuViewController? {
return nil
}
}
class FakeObjectConfigurator : ObjectConfigurator {
override func appMenuManager() -> AppMenuManager {
return FakeAppMenuManager()
}
}
appDelegate?.objectConfigurator = FakeObjectConfigurator()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
XCTAssertNil(window!.rootViewController,
"Window's root VC shouldn't be set if menu VC can't be created")
}
func testWindowHasRootViewControllerIfMenuViewControllerIsCreated() {
appDelegate?.objectConfigurator = FakeObjectConfigurator()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
XCTAssertEqual(window!.rootViewController, navController!,
"App delegate's nav controller should be the root VC")
}
func testMenuViewControllerIsRootVCForNavigationController() {
appDelegate?.objectConfigurator = FakeObjectConfigurator()
appDelegate?.application(nil, didFinishLaunchingWithOptions: nil)
let topViewController =
appDelegate?.navController?.topViewController
XCTAssertTrue(topViewController is MenuViewController,
"Menu view controlelr is root VC for nav controller")
}
// ...
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navController: UINavigationController?
var objectConfigurator: ObjectConfigurator?
func application(application: UIApplication!,
didFinishLaunchingWithOptions launchOptions: NSDictionary!)
-> Bool
{
if window == nil {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
}
if objectConfigurator == nil {
objectConfigurator = ObjectConfigurator()
}
let appMenuManager = objectConfigurator?.appMenuManager()
if let menuViewController = appMenuManager!.menuViewController() {
if navController == nil {
navController = UINavigationController()
}
navController?.viewControllers = [menuViewController]
window!.rootViewController = navController!
}
window!.makeKeyAndVisible()
return true
}
}
Let’s turn our attention to MenuViewController
. Although, all tests for it should be passing now we need to do a little refactoring. Let’s extract the code that decides which view controller should be the tap handler into a separate class. Create a new test class named MenuItemTapHandlerBuilderTests
in AppMenuTests target and replace its content with following.
import UIKit
import XCTest
class MenuItemTapHandlerBuilderTests: XCTestCase {
var tapHandlerBuilder: MenuItemTapHandlerBuilder?
var menuItem: MenuItem?
override func setUp() {
super.setUp()
tapHandlerBuilder = MenuItemTapHandlerBuilder()
menuItem = MenuItem(title: "Test menu item")
}
func testReturnsContributionsVCForContributionsMenuItem() {
menuItem?.tapHandlerName = "ContributionsViewController"
let tapHandler = tapHandlerBuilder?.tapHandlerForMenuItem(menuItem)
XCTAssertTrue(tapHandler is ContributionsViewController,
"Contributions VC should handle contributions menu item tap")
}
func testReturnsRepositoriesVCForRepositoriesMenuItem() {
menuItem?.tapHandlerName = "RepositoriesViewController"
let tapHandler = tapHandlerBuilder?.tapHandlerForMenuItem(menuItem)
XCTAssertTrue(tapHandler is RepositoriesViewController,
"Repositories VC should handle repositories menu item tap")
}
func testReturnsPublicActivityVCForPublicActivityMenuItem() {
menuItem?.tapHandlerName = "PublicActivityViewController"
let tapHandler = tapHandlerBuilder?.tapHandlerForMenuItem(menuItem)
XCTAssertTrue(tapHandler is PublicActivityViewController,
"PublicActivity VC should handle public activity menu item tap")
}
func testReturnsNilForAnyOtherMenuItem() {
menuItem?.tapHandlerName = "UnknownViewController"
let tapHandler = tapHandlerBuilder?.tapHandlerForMenuItem(menuItem)
XCTAssertNil(tapHandler,
"Tap handler is not built for an unkown menu item")
}
}
Let’s make the test pass by creating a new class named MenuItemTapHandlerBuilder
. Add it to both targets and replace its content with following.
import UIKit
class MenuItemTapHandlerBuilder {
func tapHandlerForMenuItem(menuItem: MenuItem?) -> UIViewController? {
var tapHandler: UIViewController?
if menuItem != nil {
switch menuItem!.tapHandlerName! {
case "ContributionsViewController":
tapHandler =
ContributionsViewController(
nibName: "ContributionsViewController",
bundle: nil)
case "RepositoriesViewController":
tapHandler =
RepositoriesViewController(
nibName: "RepositoriesViewController",
bundle: nil)
case "PublicActivityViewController":
tapHandler = PublicActivityViewController(
nibName: "PublicActivityViewController",
bundle: nil)
default:
tapHandler = nil
}
}
return tapHandler
}
}
Now that we have extracted the tap handler building code, we should inject MenuItemTapHandlerBuilder
as a dependency to MenuViewController
. In addition, let’s leverage the depdency injection facility we have built to configure an instance of MenuViewController
as well.
class MenuViewController: UIViewController {
// ...
var tapHandlerBuilder: MenuItemTapHandlerBuilder?
//...
func didSelectMenuItemNotification(notification: NSNotification?) {
var menuItem: MenuItem? = notification!.object as? MenuItem
if let tapHandler =
tapHandlerBuilder?.tapHandlerForMenuItem(menuItem) {
self.navigationController.pushViewController(tapHandler,
animated: true)
}
}
class ObjectConfiguratorTests: XCTestCase {
// ...
func testConfiguresAppMenuManagerCorrectly() {
// ...
XCTAssertNotNil(appMenuManager?.objectConfigurator,
"App menu manager has an object configurator")
}
func testConfiguresMenuViewControllerCorrectly() {
let menuViewController = objectConfigurator?.menuViewController()
XCTAssertNotNil(menuViewController,
"Menu view controller is not nil")
XCTAssertNotNil(menuViewController?.dataSource,
"Menu view controller has a data source")
XCTAssertNotNil(menuViewController?.tapHandlerBuilder,
"Menu view controller has a tap handler builder")
}
}
class ObjectConfigurator {
func appMenuManager() -> AppMenuManager {
// ...
appMenuManager.objectConfigurator = self
return appMenuManager
}
func menuViewController() -> MenuViewController {
let menuViewController
= MenuViewController(nibName: "MenuViewController",
bundle: nil)
menuViewController.dataSource = MenuTableDefaultDataSource()
menuViewController.tapHandlerBuilder = MenuItemTapHandlerBuilder()
return menuViewController
}
}
class AppMenuManager {
// ...
var objectConfigurator: ObjectConfigurator? = nil
//...
private func menuViewControllerFromMenuItems(menuItems: [MenuItem])
-> MenuViewController
{
let menuViewController = objectConfigurator?.menuViewController()
let dataSource = menuViewController!.dataSource
dataSource?.setMenuItems(menuItems)
return menuViewController!
}
class AppMenuManagerTests: XCTestCase {
// ...
override func setUp() {
//...
menuManager?.objectConfigurator = ObjectConfigurator()
}
}
Let’s run the app (Product > Run or ⌘R). When each menu item is tapped, the correct view controller should be pushed into the app navigation stack. Our final app design (listed below) ended up not deviating too much from the initial design. However, it is quite possible for the final design to evolve into something completely different.
Conclusion #
In this post we learned how to build a simple iOS app using TDD. Although Xcode 6 beta is a bit unstable as of this writing, XCTest itself seems to be quite stable. Despite the lack of mocking libraries such as OCMock and Kiwi, we were able to create fake objects easily and use them in our tests. Swift’s ability to create classes inside a method came very handy while creating specialized fake objects quickly.
Although Swift is a completely new language, the techniques you might have learned for testing features in Objective-C (or any other language for that matter) in the past are still applicable to Swift. We only scratched the surface of Test-Driven Development in this post. I encourage you to read the reference material listed in Further Reading section below for an in-depth examination of TDD. Hopefully, you will give TDD a try with your next iOS app. The only way to get better at designing (and testing) is by doing more of it.
The finished project is available on Github.