We’re making a kitten adoption app!

TDD TableViews - less boring than you think!

Joe Susnick
18 min readJun 22, 2017

--

Write code, run app, write code, run app. This is unpleasant. I hope to show you how writing tests first can help you write clean nice table views and break the code-run-code-run cycle.

What to expect from this tutorial:

  • We’ll write an app that displays a list of kittens with an indicator that will show if a kitten is adoptable. Selecting the cell will launch an external browser to search for that kitten
  • We’ll learn a little about TDD best practices and when to ignore them
  • We’ll borrow and steal from the WWDC 2017 talk “Engineering for Testability” to show how these techniques are 100% compatible with Apples recommended style of testing

Let’s get started!

Create a Single View Project

I’m going to call mine KittenAdoptR™. Make sure to check the Include Unit Tests option. (Note: I do not have this trademark in real life, please do not sue me)

Next, create a new unit test class and call it ViewControllerTests.

Go into the file and delete all the boilerplate so it looks like this:

Time to write a test!

You know that you are going to need a tableview so start there.

In ViewControllerTests.swift add the following test:

Notice that we’re using a guard for instantiating the controller so that we can fail our tests without crashing our test suite. Your tests are an app like any other, if your app crashes you lose out on information about tests that would otherwise have run.

Also note the call to loadViewIfNeeded() because otherwise you’ll get a headache later when your storyboard and your class aren’t speaking to each other.

Run the tests!

You’ll see an error:

Value of type ‘ViewController’ has no member ‘tableView’

Make it compile!

Open ViewController.swift, delete the boilerplate, and add the following:

@IBOutlet weak var tableView: UITableView!

Run the tests!

They (it) will fail and that is great. We have a var but no UITableView to fill it.

Make it green!

In Main.storyboard, drag a UITableView onto ViewController.

Pin it to all four sides without margins:

Run the tests!

It fails with the error “Controller should have a tableview”

Our own error message!

Make it green!

Back in Main.storyboard, select your controller, show the connections inspector, and drag from tableView to your tableview.

Run the tests!

Now they pass! So it seems like not much yet but already in one test you’ve made sure your controller has a UITableView, and that it’s present on the storyboard.

Aside: I decided to use a UIViewController with a UITableView instead of using a UITableViewController because this is a somewhat contrived example app and the techniques for testing the UITableView outlet can be applied to any IBOutlet. In real life this would be a great case for a UITableViewController.

Time to write another test!

Lets think about what we want our controller to do. I find it helpful to pseudocode some of the requirements that I want to see and then to write tests that make sure those requirements are met.

For example:

  • The table view’s dataSource should be a KittenDataSource
  • A KittenDataSource should be a test-driven thing that knows about Kittens
  • A Kitten should also be a test-driven thing
  • The controller should be the tableView’s delegate

First, a test for the dataSource:

Notice all the setup is the same as the previous test. The only difference is the assertion. We could move the duplicated setup into a setUp() method but I personally like to wait a little longer before I start moving things around.

Run the tests!

It fails with the message: “Use of undeclared type ‘KittenDataSource’”.

Make it Compile!

Create a new file and call it KittenDataSource.swift. Add a class declaration for KittenDataSource:

class KittenDataSource {}

Run the tests!

It fails with the error “TableView’s data source should be a KittenDataSource”

Time to write a Kitten test!

Now. Comment out the failing test. We can’t get it to pass without writing a KittenDataSource that conforms to UITableViewDelegate. We can’t write that without tests.

Furthermore, we can’t write tests and code for KittenDataSource because we don’t have tests for whatever a Kitten is.

Let’s start there.

Create a new test file and call it KittenTests.swift

Import the project, delete the boilerplate, and add the following test:

We’re creating a Kitten and making sure it has a name that’s set when you initialize.

Run the tests!

It fails with the message: “Use of unresolved identifier ‘Kitten’”.

Make it Compile!

Create a new file and call it Kitten.swift. Add a class declaration for Kitten:

class Kitten {}

Run the tests!

It fails to build for a pretty obvious reason.

Argument passed to call that takes no arguments

Make it Compile!

Add the following to Kitten.swift:

init(name: String) {}

Run the tests!

It fails to build since Kitten still does not have a name.

Value of type ‘Kitten’ has no member ‘name’

Make it Compile!

Update Kitten.swift to look like this:

class Kitten {
var name: String
init(name: String) {
self.name = name
}
}

Run the tests!

Green! Hooray!

Time to write another Kitten test!

Another requirement we should test for is that a Kitten should not be adoptable by default. Why? I can think of some pretty crazy reasons. Some boring ones too. This is a contrived example project, roll with it.

In Kitten.swift add the following test:

func testKittenIsNotAdoptableByDefault() {
let kitten = Kitten(name: "Uncle Fester")
XCTAssertFalse(kitten.isAdoptable,
"Kitten should not be adoptable by default")
}

Run the tests!

It fails to build since there is no property for isAdoptable.

Value of type ‘Kitten’ has no member ‘isAdoptable’

Make it Compile!

In Kitten.swift add this line below the name declaration:

var isAdoptable = false

Run the tests!

Green! You also could have accomplished this by adding it as a default parameter to the initializer but doing it that way would probably mean you’d want to add more tests.

Time to write ANOTHER Kitten test!

We also want to make sure that a Kitten can be set to adoptable.

In Kitten.swift add the following test:

func testKittenCanBeSetToAdoptable() {
let kitten = Kitten(name: "Uncle Fester")
kitten.isAdoptable = true
XCTAssertTrue(kitten.isAdoptable,
"Kitten may be set to adoptable")
}

Run the tests!

It passes! We got that one for free. You’re probably wondering why to bother writing that test. Even though it took no code to implement it tells us some important information about Kitten's isAdoptable property.

  1. isAdoptable is not under any sort of access control such as private(set)
  2. isAdoptable is not a computed variable
  3. isAdoptable is not being changed in a willSet or a didSet

Now that we have a very basic idea of a Kitten, we can jump back into testing our data source.

Time to write a KittenDataSource Test!

Create a new test file and call it KittenDataSourceTests.swift

Import the project, delete the boilerplate, and add the following test:

Here we’re seeding our data source with kittens and then checking that it has the proper number of kittens.

Run the tests!

It fails to build since we’re missing a kittens property on our KittenDataSource.

Value of type ‘KittenDataSource’ has no member ‘kittens’

Make it Compile!

In KittenDataSource.swift add the following line:

var kittens = [Kitten]()

Here we simply set kittens to an empty array of kittens.

Run the tests!

It’s green!

Time to write another KittenDataSource test!

Now we have a KittenDataSource that has Kittens but it isn’t actually a UITableViewDataSource yet.

First, a little cleanup. In KittenDataSourceTests.swift move the setup to a new method setUp(). The whole test file should now look like so:

Now add the following test to KittenDataSourceTests.swift:

Run the tests!

It fails because KittenDataSource has no method tableView(_:numberOfRowsInSection:).

Value of type ‘KittenDataSource’ has no member ‘tableView’

Make it Compile!

Side note: make sure you are importing UIKit at the top of the file.

import UIKit

Add the implementation for tableView(_:numberOfRowsInSection):

Run the tests!

It passes!

Time to write another KittenDataSource test!

Add the following test to KittenDataSourceTests.swift:

Run the tests!

It fails because it thinks you’re trying to call tableView(_:numberOfRowsInSection) with the wrong signature.

Incorrect argument label in call (have ‘_:cellForRowAt:’, expected ‘_:numberOfRowsInSection:’)

Make it Compile!

In KittenDataSource.swift, add the implementation for tableView(_:cellForRowAt:):

Run the tests!

It fails with an NSInternalInconsistencyException:

In the report navigator you can see more information:

It’s a bit of a code wall but the first line tells us what we need to know,

“unable to dequeue a cell with identifier Cell — must register a nib or a class for the identifier or connect a prototype cell in a storyboard”

Since we’re trying to test the data source without involving storyboards we’ll need a workaround.

Make it Compile!

Since we’re not using a custom cell class, we can register a cell directly. Update testCellForRow() so it reads:

Run the tests!

Now the test fails with an internal consistency error. Long story short, it’s failing because it can’t figure out what height the cell should be. Usually when you see an error that includes request for rect it means you’re missing some information related to the frame or the containing view.

Request for rect at invalid indexPath

Make it green!

Under tableView.register(UITableViewCell.self,for CellReuseIdentifier: "Cell") add another line:

//This is the standard/default UITableViewCell height
tableView.estimatedRowHeight = 44

Run the tests!

Green!

Phew. That was a long workaround just to be able to test that the original controller has a tableview with a datasource that shows it kittens. Now we can finish testing that our data source is a KittenDataSource.

In ViewControllerTests.swift, uncomment testTableViewDataSourceIsKittenDataSource.

Run the tests!

They fail still.

TableView’s data source should be a KittenDataSource

Make it green!

In ViewController.swift add a private variable to hold onto the KittenDataSource.

private var dataSource = KittenDataSource()

Then add a didSet to the tableView outlet like so:

@IBOutlet weak var tableView: UITableView! {
didSet {
tableView.dataSource = dataSource
}
}

A Tangent: The reason we can’t just write,

didSet {
tableView.dataSource = KittenDataSource()
}

is that the dataSource property of UITableView is weak. So the garbage collector looks at the object we’re setting onto our tableView.dataSource, sees only a single, weak reference to it, and cleans it up. Since the reference is weak, it’s gone.

Run the tests!

They fail because we’ve forgotten a pretty crucial detail

Cannot assign value of type ‘KittenDataSource’ to type ‘UITableViewDataSource?’

Make it Compile!

In KittenDataSource.swift update the signature so it reads:

class KittenDataSource: UITableViewDataSource {

Run the tests!

It still fails because it doesn’t conform to NSObjectProtocol:

Type ‘KittenDataSource’ does not conform to protocol ‘NSObjectProtocol’

Make it Compile!

Add NSObject to the signature so it reads:

class KittenDataSource: NSObject, UITableViewDataSource {

Run the tests!

It passes since we’ve already implemented the two required methods of UITableViewDataSource!

Time to write another ViewController test!

So right now we have an issue with our test suite. We’re testing that our dataSource works but we’re making a huge leap with the line:

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

This will work with the tableView in our KittenDataSourceTests but not in our actual app. We need a test to make sure that our tableView can also dequeue a cell correctly.

In ViewControllerTests.swift add the following test:

Still in ViewControllerTests.swift we can perform a quick refactor to move the controller into it’s own fixture. Now that we’re using it three times it makes sense to me to avoid the duplication.

Add a class-level controller var and move the setup into a setUp() method. The resulting file should look like this:

Much cleaner.

Run the tests!

It fails because we have no cell on the tableView.

Make it green!

In Main.storyboard, select the tableView and change the number of dynamic prototype cells to one.

Needs a prototype cell to dequeue.

Then select the cell and change it’s identifier to “Cell”

Cells don’t officially need an identifier but they sorta do.

Run the tests!

Green! Hooray! This sort of oversight would make our app not work.

It’s important to be careful when you’re using stand-ins for UIKit classes in your tests.

Time to write another KittenDataSource test!

Now that we have all of our tests passing, we can start thinking about what other requirements we care about. How about the number of sections?

In KittenDataSourceTests.swift add the following test:

Run the tests!

They fail to compile since we haven’t implemented ‘numberOfSections’ yet.

Make it Compile! / Make it Green!

In KittenDataSource.swift add the following:

func numberOfSections(in tableView: UITableView) -> Int {
return 0
}

Run the tests!

They pass! This is a good example of what people mean when they talk about testing behavior instead of testing implementation.

In this case the implementation is ‘wrong’. We will never be able to see our kittens since our number of sections will always be zero.

However, the behavior is correct. We want zero sections when no kittens are presents. The fact that there are zero sections when kittens are present is incidental - our requirement is met. Of course we want different behavior when kittens are present, so lets write a test for that.

Time to write another test!

In KittenDataSourceTests.swift add the following test:

Run the tests!

They predictably fail since we’re always returning zero section.

Make it green!

In KittenDataSource.swift's numberOfSections(in:) method, replace:

return 0

with:

return kittens.count > 0 ? 1 : 0

Run the tests!

… Greeen!

Now you have a very basic view controller that simply lists Kittens, before we go on to indicate whether or not they’re adoptable, let’s review. On the back on nine tests we’ve built an app that will display a (hard-coded) list of things.

  • We have an incredibly simple view controller
  • We have a separate data source that can be tested independently of the controller
  • We haven’t written a single line of code that we do not absolutely need

It’s cool, but we still only have a list of things and that is a very boring app.

Time to write another KittenDataSource test!

To show that the kittens are adoptable we could use a custom cell with special text or a button or something but for now let’s write tests to cover the following:

  • A kitten shows an indicator if it is adoptable
  • A kitten is selectable if it is adoptable

First some housekeeping. In KittenDataSourceTests.swift move the declaration and setup for tableView to the top of the file and remove all the references in the tests. The resulting file should look like this:

Now add the following test:

First we set the kitten to adoptable and ensure that it is configured and available for the test. Then we dequeue the cell at that kitten’s position and check for a disclosure indicator.

Run the tests!

It fails with a pretty useless error message:

“UITableViewCellAccessoryType” is not equal to “UITableViewCellAccessoryType”… thanks Xcode.

Make it green!

To get this to pass, go into Main.storyboard select the prototype cell from earlier and change the ‘Accessory’ to ‘Disclosure Indicator’.

Run the tests!

It fails again with the same useless error message:

“UITableViewCellAccessoryType” is not equal to “UITableViewCellAccessoryType”… thanks Xcode.

Debug?

Now we’re at a point where we know we’ve implemented it correctly. That cell should have a disclosure indicator.

What’s happening here is that we’ve reached the limit of what we can test without including the storyboard. In a longer tutorial I’d create a nib, with tests, and register that nib for the tableViews in ViewController and whatever tests are using the tableView. But to stay on track here we’re going to do this the easy way and access the tableView through the storyboard in ViewControllerTests.swift.

Copy the entire test, and move it to ViewControllerTests.swift. Update it to look like this:

Run the tests!

And they pass!

A couple takeaways before the next test:

  • Sometimes a test won’t pass without onerous workarounds. This may be a sign that your test was written in the wrong place.
  • You can easily trick yourself by using stand-ins for UIKit classes. It can save time but also lead to unexpected behavior and worse, false positives.
  • You can architect your app so that you are using the components you need where you need them — i.e. using UINibs that are registered with UITableViews so that you can better isolate and use storyboards more sparingly in your tests

Time to write another ViewController test!

Right now if we ran the app (Don’t! You’ll ruin the surprise!) we’d see a list of kittens with disclosure arrows on each kitten. This is not what we want.

Still in ViewControllerTests.swift add the following test:

Run the tests!

It fails with the same useless error message we know and love. In the future I’ll write a post about smarter error messaging:

“UITableViewCellAccessoryType” is not equal to “UITableViewCellAccessoryType”… thanks Xcode.

Make it green!

In KittenDataSource.swift add the following to tableView(_:cellForRowAt:):

if kittens[indexPath.row].isAdoptable {
cell.accessoryType = .disclosureIndicator
} else {
cell.accessoryType = .none
}

Run the tests!

They all pass! Hooray!

Time to write another test!

So now we have a list of kittens with disclosure indicators for the adoptable ones. We need to make sure that you cannot select a cell if the kitten is not adoptable.

In ViewControllerTests.swift add the following test:

func testTableViewDelegateIsViewController() {
XCTAssertTrue(controller.tableView.delegate === controller,
"Controller should be delegate for the table view")
}

Notice that we’re checking for object equality instead of using XCTAssertEqual. This is because UITableViewDelegate does not conform to the Equatable protocol and I’m not even sure what conformance to that would even look like.

Run the tests!

It fails with our own message which I’m fine with.

Controller should be delegate for the tableView

Make it green!

In Main.storyboard select the table view and drag an outlet to the view controller like so:

Run the tests!

Green! They all pass!

Time to write another test!

But first, some housekeeping. In ViewControllerTests.swift add the following just below the declaration of controller.

var tableView: UITableView!
var dataSource: KittenDataSource!
var delegate: UITableViewDelegate!

Update setUp() to the following:

override func setUp() {
super.setUp()
guard let vc = UIStoryboard(name: "Main", bundle: Bundle(for: ViewController.self))
.instantiateInitialViewController() as? ViewController else {
return XCTFail("Could not instantiate ViewController from main storyboard")
}
controller = vc
controller.loadViewIfNeeded()
tableView = controller.tableView
guard let ds = tableView.dataSource as? KittenDataSource else {
return XCTFail("Controller's table view should have a kitten data source")
}
dataSource = ds
delegate = tableView.delegate
}

Now remove the nested references throughout the rest of the tests. You’ll notice that you can now safely remove testHasTableView() and testTableViewDataSourceIsKittenDataSource() since these are both covered in the setUp().

The resulting test file should look like this:

Now we can add a test for cell selection. Add the following test:

Run the tests!

Green! They all pass! Of course.

Time to write another ViewController test!

Now we need a test for the inverse. Add the following in ViewControllerTests.swift:

Run the tests!

It fails because we haven’t implemented tableView(_:willSelectRowAt:) yet.

Delegate should allow cell selection for adoptable kittens

Make it green!

In ViewController.swift change the signature of the class to read:

class ViewController: UIViewController, UITableViewDelegate

Then add the following method:

Run the tests!

Boom! All green!

Time to talk about protocols!

Now we need to do something when the user selects a cell. If you haven’t seen the WWDC 2017 talk “Engineering for Testability”, you should watch it here. We’ll do something similar and test opening a link outside of the app.

Note: This is going to get more confusing that what we’ve done before so I’ll try and keep that in mind when introducing new concepts. Please leave comments about what I can explain more thoroughly and I’ll update this article to address those concerns. — thanks.

If we think this through, we want our UITableViewDelegate to have a way of opening a url. This is typically done through the shared instance of UIApplication. We can implement the pattern that the talk lays out by completing a few steps:

  1. Create a ‘URLOpener’ protocol says that anything conforming to it must implement a method for opening a URL
  2. Give our UITableViewDelegate a property for an ‘opener’ of the type ‘URLOpener’
    - Now our UITableViewDelegate owns an ‘opener’ that knows how to open a URL. It does not care what that ‘opener’ is, only that it knows how to open a URL
  3. Make UIApplication.shared conform to ‘URLOpener’
    - Now our UITableViewDelegate can use it as an ‘opener’ and ask it to open a URL
  4. Make UIApplication.shared the default opener for our UITableViewDelegate
    - We can do this in an initializer or as a public property. I prefer the initializer approach so that we can keep ‘opener’ private.
  5. In our tests, pass any object to our UITableViewDelegate that knows how to open a URL, but we’ll tell it to use UIApplication.shared as the default

Time to write a test to cover steps 1 and 3!

Add the following test to ViewControllerTests.swift:

This test ensures that your app has a concept of a URLOpener and that UIApplication.shared can act as one.

Run the tests!

It fails because we haven’t defined URLOpener.

Use of undeclared type ‘URLOpener’

Make it Compile!

Add the following protocol to the top of ViewController.swift:

protocol URLOpener {
func openURL(_ url: URL) -> Bool
}

Run the tests!

They fail because UIApplication.shared does not conform to URLOpener, yet.

UIApplication.shared should conform to URLOpener

Make it green!

Add the following extension to the top of ViewController.swift:

extension UIApplication: URLOpener {}

Run the tests!

They pass!

Now we can add a test to cover steps 2 and 4.

Time to write a test to cover steps 2 and 4!

Add the following in ViewControllerTests.swift:

This test ensures that your UITableViewDelegate which in this case is your controller, has a property for an opener that defaults to UIApplication.shared.

Run the tests!

They fail because ViewController doesn’t have a property for an opener.

Value of type ‘ViewController’ has no member ‘opener’

Make it Compile!

Add the following to ViewController.swift:

var opener: URLOpener = UIApplication.shared

Run the tests!

They fail for a sort of interesting reason.

Binary operator ‘===’ cannot be applied to operands of type ‘URLOpener’ and ‘UIApplication’

There’s a good explanation of the problem here, but basically the error is saying that it can’t know if a ‘URLOpener’ is a value type or a reference type and ‘===’ is only for comparing reference types so if say, two structs conformed to URLOpener then their identities (location in memory) could not be compared.

Make it Compile!

Update your URLOpener declaration to read:

protocol URLOpener: class {

Run the tests!

Green! Baboom!

Time to write another test!

Those two tests were just enabling us to test the thing we really want to test; that when you select a cell, the app tries to open a URL.

Add the following in ViewControllerTests.swift:

Run the tests!

It fails because we haven’t created a MockURLOpener().

Use of unresolved identifier ‘MockURLOpener’

Make it Compile!

At the bottom of ViewControllerTests.swift add the following declaration:

private class MockURLOpener {}

Run the tests!

Now you have three errors.

  • Value of type ‘MockURLOpener’ does not conform to ‘URLOpener’ in assignment
  • Value of type ‘MockURLOpener’ has no member ‘openURLCalled’
  • Value of type ‘MockURLOpener’ has no member ‘openURLURL’

Make it Compile!

The first error is easy to fix. Update MockURLOpener to conform to URLOpener. It should looks like this:

Run the tests!

Now you have two errors.

  • Value of type ‘MockURLOpener’ has no member ‘openURLCalled’
  • Value of type ‘MockURLOpener’ has no member ‘openURLURL’

Make it Compile!

Update MockURLOpener to spy on whether it was called or not and it’s input.

Run the tests!

It fails with our message! Finally!

Should have called openURL on url opener

Make it green!

In ViewController.swift add the following:

Run the tests!

They all pass! Hooray!

Note: The didSelectRowAt method has a glaring issue. We wrote in the possibility of an early return but didn’t write a test for that scenario. I’ll leave it to you to implement that.

If you read this far you get a really fun treat!

In ViewController.swift add the following implementation of viewDidLoad:

Run the app!

Magic! You created an entire app, albeit a simple one, without any running of the app. You were able to define the behavior you wanted in the tests. Find the whole project here.

Also please 💚 if you’re into that sort of thing. Thanks!

--

--

Responses (4)