A Cowardly Test-o-phobe's presentation from iOSDevUK

Sep 9, 2013 00:00 · 1252 words · 6 minute read

Last week I gave a presentation at iOSDevUK in Aberystwyth on test-driven development for iOS. This is the first of three posts that go through the presentation itself and some of the background, together with links to other resources. ( Part 2 | Part 3 )

It sometimes seems to me that unit testing on iOS has been a poor relation ever since the framework was released and having talked to some ex-Apple people one evening, it seems that unit testing hasn’t been an approach that Apple have used much internally. This might go some way to explaining the relatively poor state of the tools compared to say, the Rails world.

That said, with a combination of the Apple tools that are available and those created by the iOS community, unit testing and test-driven development are both possible and feasible.

It’s my contention that test-driven development delivers better-quality code, so the purpose of the talk was to try to show that building apps with this approach is something that can and should be done.

Having said that, I don’t have a lot of time for the quasi-religious waffle that tends to characterise discussion of test-driven development. Like any technical topic, there are those who lose sight of the end goal in the search for the “one true way” of doing things. I get paid by the delivered project, so I’m more pragmatic - if it helps me build bug-free code quicker, then I’ll live with the lack of ideological purity.

Bearing that in mind, what follows is here is my take on things and other opinions are available. This is the first of three posts which walks through the presentation code that can be downloaded or cloned from GitHub: https://github.com/timd/TrafficLightTests It’s not a word-for-word transcript of what I said, but this in combination with the test code itself should be clear enough.

The project

The demo project is an iPhone project that runs a simulation of a set of traffic lights running the UK sequence - red; red and amber; green; amber; and back to red. There are two sets of lights - one controlling upstream traffic; and the other controlling downstream.

The app launches with all lights off; tapping the start button launches the sequence with two red lights. From there, tapping the tick button toggles each set of lights through the sequence in turn before repeating. Tapping the stop button at any time reset the sequence back to two reds.

Before the sequence is started by tapping start, the tick and stop buttons are disabled. Once the sequence is in progress, the start button is disabled, and tick and stop are enabled.

The app structure

The app has a model-view-controller structure, and uses a state machine to control the state of the lights as they run through the sequence. The LightEngine class acts as a delegate to the ViewController and has a single tick method that returns an NSNumber light code. That’s a decimal representation of the state of the lights at each stage of the sequence, together with a flag to show whether the traffic is flowing upstream or downstream.

The ViewController class uses the code to control the state of the lights, by making a bitwise comparison of the code’s binary value and toggling each light in turn. It also responds to the start and stop buttons by resetting the lights, LightEngine and button enablement.

The source code

All the source code (including the tests) can be downloaded or cloned from GitHub: https://github.com/timd/TrafficLightTests

The testing approach

I split the talk into two sections - the first was a quick demo of how to add a test target to a project; and then install the Kiwi framework to replace the default SenTest. I’ll cover those steps in a separate, later post.

The second section looked at building the app from scratch using a test-driven approach. To do to this in 40 minutes while typing frantically was a bit of a tall order, so I cheated slightly by using Xcode snippets to store the code rather than typing; and Git to flip between the project in various states.

The structure of the testing covered three areas:

  • the LightEngine model, which returns the code for the lights at each step in the sequence
  • the user interface - wiring up the buttons to call the right methods, and control the initial state of the lights
  • the lights themselves - taking the code returned from the LightEngine and updating the display accordingly.

That’s by no means the only approach that could be used; but it makes logical sense to me to start from the inside-out by building the model; getting the model to respond to user input; then fully-updating the interface.

# Testing the model

The model is a standalone NSObject subclass that has one role in life - to act as a state machine that steps through each permutation of lights in turn, and respond with a code to a tick method. It’s linked to the view controller as the delegate - this isn’t necessarily an architecture that you’d use in real life, but it was a handy way of demonstrating testing of delegates.

“Classical” TDD starts with a blinking cursor in an empty text file, and works from there. That’s not possible with an Xcode project, because the compiler will complain about missing classes - so the first step is to test that the object can be instantiated. Also included in that batch are tests for the existence of the tick method, and that an NSNumber is returned:

context(@"when instantiated", ^{

    it(@"should exist", ^{
        [[engine shouldNot] beNil];
    });

    it(@"should respond to the 'tick' message", ^{
        [[engine should] respondToSelector:@selector(tick)];
    });

    it(@"should return an NSNumber in response to a 'tick' message", ^{
        id result = [engine tick];
        [[result should] beKindOfClass:[NSNumber class]];
    });

});

Next, the tick method is tested to ensure that it’ll handle keeping track of the position in the state machine sequence:

describe(@"and handling the tick count", ^{

    it(@"should have a tickCount property", ^{
        [[engine should] respondToSelector:@selector(tickCount)];
    });

    it(@"should increment the tickCount property in response to a tick", ^{
        [[theValue(engine.tickCount) should] equal:theValue(0)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(1)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(2)];
    });

    it(@"should roll the tickCount over to 0 when it reaches 7", ^{
        [[theValue(engine.tickCount) should] equal:theValue(0)];
        for(int count=0; count<7; count++) {
            [engine tick];
        }
        [[theValue(engine.tickCount) should] equal:theValue(7)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(0)];
    });

});

And finally that the tick method will return the right values at the right point in the sequence:

describe(@"and handling tick responses", ^{

    it(@"should return @164 in response to the first tick", ^{
        NSNumber *result = [engine tick];
        [[result should] equal:@164];
    });

    it(@"should return @180 in response to the second tick", ^{
        [engine tick];
        NSNumber *secondResult = [engine tick];
        [[secondResult should] equal:@180];
    });

    NSArray *expectedValues = @[@164,@180,@140,@148,@100,@102,@97,@98,
                                @164,@180,@140,@148,@100,@102,@97,@98,
                                @164,@180,@140,@148,@100,@102,@97,@98];

    it(@"should return the correct values when ticking repeatedly", ^{

        for (NSNumber *value in expectedValues) {
            NSNumber *returnValue = [engine tick];
            [[returnValue should] equal:value];
        }

    });

});

The final set of tests is to check that the model will handle being brought to a halt:

context(@"when stopping", ^{

    it(@"should respond to the stop message", ^{
        [[engine should] respondToSelector:@selector(stopSequence)];
    });

    it(@"should reset the tickCount back to zero in response to a stop message", ^{
        [[theValue([engine tickCount]) should] equal:theValue(0)];
        [engine tick];
        [[theValue([engine tickCount]) should] equal:theValue(1)];
        [engine stopSequence];
        [[theValue([engine tickCount]) should] equal:theValue(0)];
    });

});

Testing the user interface

In the next post, I’ll cover setting up the user interface for testing; and step through how to test user interaction.

http://adoptioncurve.net/archives/2013/09/testing-for-cowards-part-2-testing-user-interfaces/