Test Driving iOS - A Primer

Gordon Fontenot

In case you missed it, our own Britt Ballard (a fellow native Texan) published a fantastic post in our Back to Basics series on test-first methodology. My only issue with it is that the entire post is written in this super obscure language called Ruby! So I wanted to take a look at the same methodology from the point of view of an iOS developer, in tried and true Objective-C.

Our desired code

We would like to build a system that includes the following:

  • A category method for reversing strings
  • A custom UITableViewCell subclass with a method that:
    • Takes a string
    • Reverses the string
    • Sets the string as the text on its label

Tools and techniques

We’re going to use the same techniques as laid out in Britt’s post. We’ll use red/green/refactor methodology, and we’ll write unit tests that are fast, isolated, repeatable, self-verifying, and timely. But instead of using RSpec, we’re going to use Specta and Expecta to test our method’s output. These are modular spec and matcher frameworks (respectively) that make XCTest not horrible. We’ll also use OCMock to mock out any external dependencies we might run into.

A quick aside on using Specta

Assuming you’re using CocoaPods, you’ll want to create a group inside your Podfile for our dependencies:

target :unit_tests, :exclusive => true do
  link_with 'UnitTests'
  pod 'Specta'
  pod 'Expecta'
  pod 'OCMock'
end

This will keep our testing dependencies out of our main bundle, and the :exclusive => true flag will keep any normal dependencies out of our testing bundle. This is to avoid duplicate symbols when the testing bundle is injected into the host application.

Specta has a syntax you might not be immediately familiar with. I highly recommend installing their file templates, but all you really need to know is that instead of your normal pattern for defining a class:

@implementation MyTestClass : XCTestCase

// Tests go here

@end

Specta uses some nifty macros to clean things up:

SpecBegin(ClassUnderTest)

// Tests go here

SpecEnd

Convention (stolen from Ruby) says that we should name our spec files ClassUnderTestSpec.m. Specs for categories will be named FoundationClass_ExtensionSpec.m. We don’t need public headers for specs.

The testing syntax itself is very block heavy. But other than that, it follows RSpec’s syntax almost exactly.

The category method: Starting with the desired end state

We’ll start in the same place as we would in Ruby. Given a string (“example string”), we expect to be able to call a method to get a reversed version of that string (“gnirts elpmaxe”):

// NSString_ReversedSpec.m

#import "NSString+Reversed.h"

SpecBegin(NSString_Reversed)

describe(@"NSString+Reversed", ^{
    it(@"reverses strings", ^{
        NSString *originalString = @"example string";

        NSString *reversedString = [originalString reversedString];

        expect(reversedString).to.equal(@"gnirts elpmaxe");
    });
});

SpecEnd

Now, here’s where Ruby and Objective-C diverge a bit. If we look at Britt’s original post, this is when he starts running the tests. But since we have a compiler, and Xcode generates warnings and errors in real time, we’re able to lean on those a bit to get us moving forward.

Right now we should have 2 errors displayed:

  • 'NSString+Reversed.h' file not found for our #import statement
  • No visible @interface for 'NSString' declares the selector 'reversedString'

This is the roadmap for our first few steps. Looking at the errors, we can see that we need to do 2 things:

  • Create a new Reversed category on NSString
  • Define a public reversedString method for that category

Let’s go ahead and take care of the first one:

// NSString+Reversed.h

@interface NSString (Reversed)
@end

// NSString+Reversed.m

#import "NSString+Reversed.h"

@implementation NSString (Reversed)
@end

Once Xcode catches up, we should see one of the errors disappear. The only remaining error is telling us that we need to define reversedString. We know by looking at the usage that it’s going to need to return an NSString. So let’s go ahead and declare it:

// NSString+Reversed.h

@interface NSString (Reversed)

- (NSString *)reversedString;

@end

And hey! Just like that, we’ve gotten rid of all of the errors that were preventing us from building. But there’s still one more thing we can use the compiler to lead us to, before actually running the tests.

Once the errors went away, we should have seen a warning pop up (you might have to build the project to get Xcode to catch up). The warning tells us that while we’re saying that NSString has a method named reversedString, there isn’t actually any implementation for a method with that name anywhere. Let’s implement that method just enough to get rid of the warning:

// NSString+Reversed.m

#import "NSString+Reversed.h"

@implementation NSString (Reversed)

- (NSString *)reversedString
{
    return nil;
}

@end

Blam. No warnings, no errors. We haven’t run the tests once, but we’ve used the compiler to help us get to the point where we know that our class is set up the way we said it would be. Let’s finally run the tests! I recommend getting used to smashing ⌘U in Xcode. It’s a handy shortcut. In the test navigator we see the failure:

''

And we see the error inline:

expected: gnirts elpmaxe, got: nil/null

So now we know that everything is hooked up properly, but we aren’t returning the right thing. Let’s make the test pass with the least amount of work possible:

// NSString+Reversed.m

#import "NSString+Reversed.h"

@implementation NSString (Reversed)

- (NSString *)reversedString
{
    return @"gnirts elpmaxe";
}

@end

And now we have our first passing test!

''

Refactoring

Returning a hardcoded value got our tests to pass, but it surely isn’t what we want in production. It’s pretty obvious that this method won’t work for any string value, but now that we’ve gotten our test suite (small that it may be) to green, we can use it to help us complete the implementation without changing the expected behavior:

// NSString+Reversed.m

#import "NSString+Reversed.h"

@implementation NSString (Reversed)

- (NSString *)reversedString
{
    NSMutableString *reversedString = [NSMutableString string];

    NSRange range = NSMakeRange(0, [self length]);
    NSStringEnumerationOptions options = (NSStringEnumerationReverse | NSStringEnumerationByComposedCharacterSequences);

    [self enumerateSubstringsInRange:range
                               options:options
                            usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
                                [reversedString appendString:substring];
                            }];

    return [reversedString copy];
}

@end

Now we have a nice method that we can use on any NSString instance that will return a reversed version of that string, and the whole thing is backed by tests!

UITableViewCell subclass

We start our journey the same as before. We’ll state our expected behavior and then work towards getting a clean build, and a green test suite.

We’ll start with the initial requirement of reversing the string provided to the method. We’re going to use a mock object to ensure that we’re only dealing with our Foundation class and the class under test:

// TBReverseStringCellSpec.m

#import "TBReverseStringCell.h"

SpecBegin(TBReverseStringCell)

describe(@"TBReverseStringCell", ^{
    it(@"reverses the string provided to it", ^{
        TBReverseStringCell *cell = [TBReverseStringCell new];
        id stringMock = [OCMockObject mockForClass:[NSString class]];
        [[stringMock expect] reversedString];

        [cell setReversedText:stringMock];

        [stringMock verify];
    });
});

SpecEnd

Right away, we notice that we’ve got a few errors to deal with. We’ll start by creating the class itself:

// TBReverseStringCell.h

@interface TBReverseStringCell : UITableViewCell
@end

// TBReverseStringCell.m

#import "TBReverseStringCell.h"

@implementation TBReverseStringCell
@end

Now we’re left with the warning about the undeclared selector, so we’ll fix that:

// TBReverseStringCell.h

@interface TBReverseStringCell : UITableViewCell

- (void)setReversedText:(NSString *)string;

@end

And now we’re left with that same warning about the missing definition for setReversedText:. Once again, we’re only going to add enough code to get our project to build cleanly. In this case, that means creating an empty method definition:

// TBReverseStringCell.m

#import "TBReverseStringCell.h"

@implementation TBReverseStringCell

- (void)setReversedText:(NSString *)string
{
}

@end

Now that our build is clean, we can run our tests, and get some meaningful feedback:

test failure: -[TBReverseStringCellSpec TBReverseStringCell_reverses_the_string_provided_to_it] failed:
OCMockObject[NSString]: expected method was not invoked: reversedString

So let’s go ahead and fix that test:

// TBReverseStringCell.m

#import "TBReverseStringCell.h"
#import "NSString+Reversed.h"

@implementation TBReverseStringCell

- (void)setReversedText:(NSString *)string
{
    [string reversedString];
}

@end

And now our suite is green again. We can move on to our final requirement, that the cell sets the reversed string value on the internal label:

// TBReverseStringCellSpec.m

it(@"sets a string value on the internal label", ^{
    TBReverseStringCell *cell = [TBReverseStringCell new];
    id labelMock = [OCMockObject mockForClass:[UILabel class]];
    cell.reverseLabel = labelMock;
    [[labelMock expect] setText:@"gnirts elpmaxe"];

    [cell setReversedText:@"example string"];

    [labelMock verify];
});

Again, a build error because of a missing part of our public interface, so that’s our first step:

// TBReverseStringCell.h

@interface TBReverseStringCell : UITableViewCell

@property (nonatomic) UILabel *reverseLabel;

- (void)setReversedText:(NSString *)string;

@end

With our build clean, we can see what we need to to to fix the test:

test failure: -[TBReverseStringCellSpec TBReverseStringCell_sets_a_string_value_on_the_internal_label] failed:
OCMockObject[UILabel]: expected method was not invoked: setText:@"gnirts elpmaxe"

A quick modification to our implementation gets us to our desired result:

// TBReverseStringCell.m

- (void)setReversedText:(NSString *)string
{
    NSString *reverseString = [string reversedString];
    self.reverseLabel.text = reverseString;
}

And there we have it. We were able to satisfy our requirements, and we did it by leading ourselves with tests. This example is simple and contrived, but this same pattern can be applied to requirements of any size.

What’s next

If you’re excited about using TDD in your app, you should check out: