How to TDD with CATCH

Plenty of people have asked me about the TDD framework I use. While the book Test-Driven iOS Development has code using OCUnit (for pragmatic, and previously-covered, reasons); I am currently more frequently to be discovered using Phil Nash’s CATCH framework. Here’s how you can get from New Project to first passing test. I’m starting in the same place that the book’s sample code starts: I need a Topic class that has a name.

Set up the project.

File->New Project in Xcode. Create an app project that matches your needs: an empty app will be fine. I’ll assume in this project, just as in the book, that you’ll be using Automatic Reference Counting for this project.

Now you need to add another app target. The “Empty Application” template is sufficient for this, and I call this target “Unit Tests”. When you’ve got that target set up, remove the app delegate class and the main.m file: we’ll add a test runner from CATCH.

For this target, you also have to choose whether or not to use ARC. If you enabled ARC for the app target, then the classes you’re testing will use Automatic Reference Counting, but the CATCH internals cannot because they use scary runtime hacks to dynamically generate classes from the test cases. In this walkthrough, you should enable ARC but we’ll have to edit compiler flags for a couple of files to get manual reference counting where we need it.

Check out CATCH from the GitHub project linked above. Then go into the Build Rules for your Unit Tests target, and add the path to the Catch/include source folder. For example, if you clone CATCH to the top-level folder of your Xcode project, you’ll need to add "$(SRCROOT)/Catch/include".

Locate the file Catch/projects/runners/iTchRunner/itChRunnerMain.mm (capitalisation as supplied) in Finder, and drag it onto your Xcode project. Add this file to the Unit Tests target. Now you need to go to the Build Phases inspector for your target, and under Compile Sources, next to itChRunnerMain.mm add the flag -fno-objc-arc.

Add the first test

Add a new file to the Unit Tests target, called TopicTests.mm (you don’t need the matching header file). As with itChRunnerMain.mm, you’ll need to set the -fno-objc-arc flag in the Build Phases. Include the catch.hpp header and write the test that checks for the name:

#include "catch.hpp"

TEST_CASE("Topic/Name", "Require that Topic has a name") {
    Topic *topic = [[Topic alloc] initWithName: @"iPhone"];
    REQUIRE(topic != nil);
    REQUIRE([topic.name isEqualToString: @"iPhone"]);
    [topic release];
}

Get this test to compile

So currently, the test target won’t even build: we’re using a Topic class and its name property, and these don’t exist. Add a new file to the project, an NSObject class called Topic. You could add this file to both targets as it will be app code: don’t disable ARC here because the whole point is you want to write automatically reference-counted app code.

Just to fast-forward things a bit, here’s an implementation of Topic.[hm] that contains minimal implementations of the required methods.

Topic.h
#import <Foundation/Foundation.h>

@interface Topic : NSObject

@property (readonly) NSString *name;

- (id)initWithName: (NSString *)aName;

@end

Topic.m
#import "Topic.h"

@implementation Topic

@synthesize name = _name;

- (id)initWithName:(NSString *)aName {
    if (self = [super init]) {
    }
    return self;
}

@end

Observe that the test fails.

Run the “Unit Tests” app in Xcode. You’ll see a screen like this:

Default Catch Screen

It’s not very exciting, but at least you can tell what to do: tap the “Run All Tests” button.

Catch failed test

Still not exciting, and it’s possible that if you have certain types of colour-blindness it’s not clear what’s happened. Luckily the logs are more explicit:

2012-03-13 17:18:01.640 UnitTests[960:707] Application windows are expected to have a root view controller at the end of application launch

2012-03-13 17:18:03.915 UnitTests[960:707] topic != __null succeeded for: 0x2d11b0 != __null

2012-03-13 17:18:03.917 UnitTests[960:707] [topic.name isEqualToString: @"iPhone"] failed for: false

2012-03-13 17:18:03.923 UnitTests[960:707] 1 failures

Make this test pass

There’s only one line to add to the app code to get everything passing. Here’s the Topic implementation again with the change highlighted:

@implementation Topic

@synthesize name = _name;

- (id)initWithName:(NSString *)aName {
    if (self = [super init]) {
        _name = [aName copy];
    }
    return self;
}

@end

Run the tests again:

Passing CATCH tests

2012-03-13 17:46:17.644 UnitTests[1049:707] Application windows are expected to have a root view controller at the end of application launch

2012-03-13 17:46:43.746 UnitTests[1049:707] topic != __null succeeded for: 0x2ca7f0 != __null

2012-03-13 17:46:43.748 UnitTests[1049:707] [topic.name isEqualToString: @"iPhone"] succeeded for: true

2012-03-13 17:46:43.753 UnitTests[1049:707] no failures

Win.

Conclusions

The runner for CATCH tests is currently very basic: it lets you know when things have passed or failed but not which tests have failed, though the tests are named. You need to manually prod it to get tests to run. However, the syntax is a refreshing change from the verbosity of JUnit-derived test frameworks.

About Graham

I make it faster and easier for you to create high-quality code.
This entry was posted in code-level, software-engineering, TDD. Bookmark the permalink.

One Response to How to TDD with CATCH

  1. Phil Nash says:

    Thanks for writing this, Graham. Always please to see more coverage of Catch :-)
    Your timing is terrible, though! The iOS client app has been languishing as a prototype for quite some time now – and I’ve had the ARC issue on my backlog for a while too. I’m just getting around to looking at those now!
    So hopefully things should start looking a lot better soon.

    I’ve spent the last couple of weeks with a client doing iOS TDD coaching. Due to the limitations of Catch for iOS at this time I couldn’t recommend they use it – so we went with GHUnit instead.
    Aside from the limitations of GHUnit you bring out in your book (mostly shared with OCUnit) I’ve felt like it’s a really blunt tool for an important job. It’s really driven home to me my motivations for starting Catch in the first place! (if the author if GHUnit is reading this – no offence intended – it does a good job of extending OCUnit).

    One area I’ve already improved on, however, is some initial support for Matchers. I’ll be looking to expand on those too – especially for iOS – but so far I have a few string and substring matchers. So your test could be written as:

    REQUIRE_THAT( topic.name, Equals( @”iPhone” ) );

    In the failure case you’ll get a message like:

    topic.name Equals( @”iPhone” ) failed for: nil equals string: @”iPhone”

    – which is much nicer.
    With Catch you normally get that level of detail for free if you use standard comparison operators (==, !=, > etc) – but that breaks down when comparing Objective-C objects, which is why I wanted to get the Matchers in.
    Matcher are also good for testing derived values – e.g. substrings. For example it’s now trivial to test that a string *contains* another string – and the reporting retains all the important information too.

    I’ll soon be adding Matchers for more advanced things, such as testing if an array has certain elements.

    I’d better stop there before this comment becomes bigger than the original post :-)

Comments are closed.