Principles of test-driven development

We could write (and people have written) whole books on what test-driven development really means, but to keep it simple, we’re going to stick to the core idea: We must not write any application code until a failing test motivates us to do so.

The Story

As a reminder, here’s the story we’re working on:

As a user,
I should be able to see a list of recipes.
so I can cook a delicious dinner
Scenario: Users looks at the recipe list
Given I have the recipe app installed
When I open the recipe app
Then I should see a list of recipes

Where to begin? The story says that when I open the app, I should see a list of recipes. Right now when we open the app, we see a list, but there’s nothing in it. That’s because right now, we’re setting the rootViewController to be a generic UITableViewController, which doesn’t do what we need.

So we’ll have to create a subclass of UITableViewController to manage the recipes list; we’ll call it BDDRecipesViewController. But before we actually create this new class, we’ll modify the BDDAppDelegateSpec to reflect our new understanding:

When we try to run the tests (⌘-U), the build fails to compile, with this error:

'BDDRecipesViewController.h' file not found

We treat build failures like test failures, i.e. as guides to tell us how to move forward with our code. In this case, the build failure tells us that we need to create a BDDRecipesViewController class.

Create BDDRecipesViewController

Create a subclass of UITableViewController called BDDRecipesViewController. Run the tests; the build succeeds this time, but the test fails with this error:

FAILURE BDDAppDelegate when the app is finished loading should display recipes
/Users/pivotal/workspace/Recipes/Specs/BDDAppDelegateSpec.mm:23 Expected < (UITableViewController)> to be an instance of class <BDDRecipesViewController>

In other words, even though we’ve created BDDRecipesViewController, we haven’t actually set it to be the root view controller of the window. Let’s do that now. In BDDAppDelegate.m, create an instance of BDDRecipesViewController and set it to be the window‘s rootViewController. Don’t forget to #import the header file:

Run the tests. The build succeeds and all the tests pass—in the language of TDD, we’ve “gone green”. Now it’s time to add the tests we need for the list of recipes.

Test-driving the number of sections

As you may know, UITableViews have a dataSource which they call to get the information they need to display the table onscreen, and typically, the UITableView’s view controller is that dataSource. So BDDRecipesViewController needs to implement the required methods in the UITableViewDataSource protocol, which are:

-numberOfSectionsInTableView:

-tableView:numberOfRowsForSection:

-tableView:cellForRowAtIndexPath:

Of course, before we actually write any BDDRecipesViewController code, we need to create a spec file to hold the tests that will describe its behavior:

Now that we have a spec file, we can start writing the tests that will drive out the data source behavior we need. We’ll start with -numberOfSectionsInTableView. We know that we’ll only need one section, so we’ll assert that -numberOfSectionsInTableView: returns 1.

Add a unit test (which is a block that holds assertions), and write a description of what that assertion is testing; in this case, that -numberOfSectionsInTableView: returns 1.

Create an instance of viewController, which we’ll use to run the assertion we’re about to write.

This line may seem mysterious. Of course the viewController‘s view should not be nil, but why write an assertion to test it? We need to write this because UIViewControllers don’t actually create their views until the first time view is called, and so if we don’t call view on viewController, there won’t be a tableView to test on the next line.

Here’s the heart of this test: When we call -numberOfSectionInTableView: with viewController‘s tableView, it should return 1.

FAILURE BDDRecipesViewController -tableView:numberOfRowsInSection: should return the number of rows for the table view
/Users/pivotal/workspace/Recipes/Specs/BDDRecipesViewControllerSpec.mm:27 Expected <0> to equal <4>

Fixture data

Where did 4 come from? When writing tests, we don’t actually want to test against real data, because real data is unpredictable; it can and probably will change over time, and that can lead to brittle tests. Instead, we’ll create some fake data to use for testing purposes. Add this line to test:

recipes is our fake data, and it has four elements; when we run the test, we expect that -tableView:numberOfRowsInSection: should return 4.

But we can’t yet run the tests, because the build fails. This is because viewController doesn’t have a recipes property. Indeed, BDDRecipesViewController has no model object. Did we overlook this very basic principle of MVC architecture? No, we simply waited for the tests to tell us we needed a model—and now they have. Add a property recipes to BDDRecipesViewController.h:

FAILURE BDDRecipesViewController -tableView:numberOfRowsInSection: should return the number of rows for the table view
/Users/pivotal/workspace/Recipes/Specs/BDDRecipesViewControllerSpec.mm:31 Expected <0> to equal <4>

That’s fine; it just means we’re ready now to actually implement -tableView:numberOfRowsInSection: in BDDRecipesViewController.m:

At this point, you may have noticed that we’ve duplicated some code in the tests, specifically the creation of viewController. We can refactor our tests to avoid this repetition by using a beforeEach block. We’ll also pull the recipes into the beforeEach so that we can use them again in the tests that follow. We can use afterEach to clean up the tests after they’ve run. Refactor the tests so that they look like this:

Test-driving the creation of cells and display of data

The last method we need to implement is -tableView:cellForRowAtIndexPath:. To test this, we want to know that the table view cells returned by the method contain the correct content. Add this describe block below the previous ones:

When we run the tests, they don’t all pass, but instead of a failure, we actually see an exception instead:

EXCEPTION BDDRecipesViewController -tableView:cellForRowAtIndexPath should return a table view cell with the right label
unable to dequeue a cell with identifier UITableViewCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard

As you may know, in iOS 6 Apple introduced an improved way of loading table view cells. Instead of explicitly calling alloc and init to create a new cell, we can simply call -dequeueReusableCellWithIdentifier:forIndexPath:, and if a new cell needs to be created, the table view will create it for us. In order for that to work, though, we need to tell the table view what type of table view cell to use for each cell identifier, and we do this by registering the UITableViewCell class in -viewDidLoad:

Conclusion

We’ve successfully test-driven our table view controller to display a list of recipes! Of course, when you run the actual app, you still don’t see any recipes appear, and that’s because we don’t have any actual recipe data. In the next post, we’ll examine how to test-drive the model classes for Recipes so that our users can create real recipe data.

Remember that the core principle behind TDD is that we don’t write any app code until some test failure motivates us to do so. By test-driving our feature, we ensure that we’re writing correct code at all times; of course, this requires that the tests adequately cover the functionality we expect from the app, and indeed that’s one of the challenges of TDD. Well-written tests not only ensure correct code, however, they also act as a sort of self-documentation for the code. Reading the describe and it blocks, it’s really easy to get a good understanding of what the code is supposed to do. And because we’ve written good tests, we can forge ahead with confidence, knowing that as long as our tests pass, any new code we write won’t break our existing code.