bdd cedar ios labs tdd

Test Driven iPhone Development with Cedar, Part II

Co Author: Andy Pliszka

In Test Driven iPhone Development with Cedar, Part I, we created a new Xcode project called Recipes and set it up to use Cedar for test-driven development. In this post, we’ll test-drive our first piece of actual functionality. All code is posted to GitHub with tags for each major breakpoint so you can pick up at any part in the series.

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:

#import "BDDAppDelegate.h"
#import "BDDRecipesViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(BDDAppDelegateSpec)

describe(@"BDDAppDelegate", ^{
    __block BDDAppDelegate *delegate;

    beforeEach(^{
        delegate = [[[BDDAppDelegate alloc] init] autorelease];
    });

    context(@"when the app is finished loading", ^{
        beforeEach(^{
            [delegate application:nil didFinishLaunchingWithOptions:nil];
        });

        it(@"should display recipes", ^{
            delegate.window.rootViewController should be_instance_of([BDDRecipesViewController class]);
        });
    });
});

SPEC_END

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:


#import "BDDAppDelegate.h"
#import "BDDRecipesViewController.h"

@implementation BDDAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[BDDRecipesViewController alloc] init];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

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:

Create BDDRecipesViewControllerSpec.mm:

Screen Shot 2013-07-25 at 2.00.13 PM

Remember to add it to the Specs target:

Screen Shot 2013-07-25 at 2.01.17 PM


#import "BDDRecipesViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(BDDRecipesViewControllerSpec)
    describe(@"BDDRecipesViewController", ^{
       __block BDDRecipesViewController *viewController;

    beforeEach(^{

    });
});

SPEC_END

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.


#import "BDDRecipesViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(BDDRecipesViewControllerSpec)

describe(@"BDDRecipesViewController", ^{
    __block BDDRecipesViewController *viewController;

    describe(@"-numberOfSectionsInTableView:", ^{ // 1
        it(@"should return 1", ^{ // 2
            viewController = [[BDDRecipesViewController alloc] init]; // 3

            viewController.view should_not be_nil; // 4

            [viewController numberOfSectionsInTableView:viewController.tableView] should equal(1); // 5

            [viewController release];
        });
    });
});

SPEC_END

What do these lines mean?

  1. Add describe section for tests of -numberOfSectionsInTableView:
  2. 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.
  3. Create an instance of viewController, which we’ll use to run the assertion we’re about to write.
  4. 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.
  5. Here’s the heart of this test: When we call -numberOfSectionInTableView: with viewController‘s tableView, it should return 1.

Run the tests; as expected, we get a failure:

FAILURE BDDRecipesViewController -numberOfSectionsInTableView: should return 1
/Users/pivotal/workspace/Recipes/Specs/BDDRecipesViewControllerSpec.mm:17 Expected <0> to equal <1>

So now we just have to implement -numberOfSections in BDDRecipesViewController.m:


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

Run the tests; now they all pass.

Test-driving the number of items

Now we need to test-drive -tableView:numberOfRowsInSection:. Add this describe block below the describe block for -numberOfSectionsInTableView::


describe(@"-numberOfSectionsInTableView:", ^{
    it(@"should return 1", ^{
        viewController = [[BDDRecipesViewController alloc] init];

        viewController.view should_not be_nil;

        [viewController numberOfSectionsInTableView:viewController.tableView] should equal(1);

        [viewController release];
    });
});

describe(@"-tableView:numberOfRowsInSection:", ^{
    it(@"should return the number of rows for the table view", ^{
        viewController = [[BDDRecipesViewController alloc] init];

        viewController.view should_not be_nil;

        [viewController tableView:viewController.tableView numberOfRowsInSection:0] should equal(4);

        [viewController release];
    });
});

Run the tests and they fail with the following error:


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:


describe(@"-tableView:numberOfRowsInSection:", ^{
    it(@"should return the number of rows for the table view", ^{
        viewController = [[BDDRecipesViewController alloc] init];

        viewController.recipes = @[@"Hamburgers", @"Guacamole", @"Bigos", @"Spaghetti"];

        viewController.view should_not be_nil;

        [viewController tableView:viewController.tableView numberOfRowsInSection:0] should equal(4);

        [viewController release];
    });
});

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:


@interface BDDRecipesViewController : UITableViewController

@property (nonatomic, copy) NSArray *recipes;

@end

Now our tests build, but they still fail:


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:


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.recipes.count;
}

Run the tests; they all pass.

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:


#import "BDDRecipesViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(BDDRecipesViewControllerSpec)

describe(@"BDDRecipesViewController", ^{
    __block BDDRecipesViewController *viewController;
    __block NSArray *recipes;

    beforeEach(^{
        viewController = [[BDDRecipesViewController alloc] init];
        recipes = @[@"Hamburgers", @"Guacamole", @"Bigos", @"Spaghetti"];
    });

    afterEach(^{
        [viewController release];
    });

    describe(@"-numberOfSectionsInTableView:", ^{
        it(@"should return 1", ^{
            viewController.view should_not be_nil;

            [viewController numberOfSectionsInTableView:viewController.tableView] should equal(1);
        });
    });

    describe(@"-tableView:numberOfRowsInSection:", ^{
        it(@"should return the number of rows for the table view", ^{
            viewController.view should_not be_nil;

            viewController.recipes = recipes;

            [viewController tableView:viewController.tableView numberOfRowsInSection:0] should equal(4);
        });
    });
});

SPEC_END

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:


describe(@"-tableView:cellForRowAtIndexPath", ^{
    it(@"should return a table view cell with the right label", ^{
        viewController.view should_not be_nil;

        viewController.recipes = recipes;

        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
        UITableViewCell *cell = [viewController tableView:viewController.tableView cellForRowAtIndexPath:indexPath];

        cell.textLabel.text should equal(@"Hamburgers");
    });
});

Run the tests, and we get this failure:


FAILURE BDDRecipesViewController -tableView:cellForRowAtIndexPath should return a table view cell with the right label
/Users/pivotal/workspace/Recipes/Specs/BDDRecipesViewControllerSpec.mm:44 Expected <(null)> to equal <Hamburgers>

So now we just have to implement the method:


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];

    cell.textLabel.text = self.recipes[indexPath.row];

    return cell;
}

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:


- (void)viewDidLoad {
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
}

Add this code, run the tests, and they all pass.

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.