Testing for cowards Part 3: Testing the full interface

Sep 9, 2013 00:00 · 1591 words · 8 minute read

Introduction to part 3

This is the third of three posts (part 1 | part 2 )that works thorough the presentation I gave at September’s iOSDevUK conference in Aberystwyth. In the first, I covered the background to test-driven development of the simple traffic lights project I’m using as an example; and looked at building the app’s model layer using a test-driven approach. The second covers testing user interaction by exposing the methods that underlie the interface.

The code and tests can be cloned or downloaded from GitHub: https://github.com/timd/TrafficLightTests.

Testing the lights

Once the model and user interaction is tested, the final piece of the jigsaw is testing that the user interface can be successfully updated by the model. This is a somewhat arbitrary division of testing, and I will probably approach things differently in another project.

Having said that, the model-view-controller structure of the app means that there’s something of a natural division between the way that the user interacts with the model (mediated through the user interface) and the way in which the user interface is updated as a result of the model’s behaviour.

The view controller is responsible for handling the lights code returned by the LightEngine and updating the display accordingly. The code is a decimal version of the binary representation of the lights:

The first set of tests check that the updateLightsForCode: method works correctly:

context(@"when working through the sequence", ^{

    it(@"should respond to the updateLightsForCode: method", ^{
        [[vc should] respondToSelector:@selector(updateLightsForCode:)];
    });

    it(@"should show Rxx Rxx when sent the @164 code", ^{
        [vc updateLightsForCode:@164];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show RAx Rxx when sent the @180 code", ^{
        [vc updateLightsForCode:@180];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor yellowColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show xxG Rxx when sent the @140 code", ^{
        [vc updateLightsForCode:@140];

        [[vc.upRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor greenColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show xAx Rxx when sent the @148 code", ^{
        [vc updateLightsForCode:@148];

        [[vc.upRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor yellowColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show Rxx Rxx when sent the @100 code", ^{
        [vc updateLightsForCode:@100];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show Rxx RAx when sent the @102 code", ^{
        [vc updateLightsForCode:@102];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor yellowColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show Rxx xxG when sent the @97 code", ^{
        [vc updateLightsForCode:@97];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor greenColor]];
    });

    it(@"should show Rxx xAx when sent the @98 code", ^{
        [vc updateLightsForCode:@98];

        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor yellowColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

});

It’s worth noting here that whereas normally you’d try to write code with the minimum of redundancy consistent with readability, with tests that’s not the case. You want the tests to be as clear as possible, and if that means writing lots of code, well, that’s what copy-and-paste was invented for.

It should be immediately obvious what these tests are about, becasue they’re written out in long form. They could be much more concise with a for-each loop and an array of values to iterate across - but then there would be two cognitive loads: one to understand the mechanics of the test, and one to understand the test itself.

Once the operation of the updateLightsForCode: method is proven, then we can look at hooking it up to the UI:

it(@"should update the lights to RAx Rxx after a first tick", ^{

    [[[vc.upRed backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

    [[[vc.downRed backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

    [vc didTapStartButton:nil];

    [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
    [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

    [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
    [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

    [vc didTapTickButton:nil];

    [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
    [[[vc.upAmber backgroundColor] should] equal:[UIColor yellowColor]];
    [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

    [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
    [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
    [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

});

This tests operation in response to the tick button being tapped once - the next stage is to try the whole sequence:

it(@"should run through the whole sequence correctly", ^{

    [vc didTapStartButton:nil];

    // Run through the whole sequence 25 times

    for(int count=0; count < 25; count++) {

        // RYB RBB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor yellowColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // BBG RBB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor greenColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // BYB RBB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor yellowColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // RBB RBB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // RBB RYB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor yellowColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // RBB BBG
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor greenColor]];

        // RBB BYB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor yellowColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

        // RBB RBB
        [vc didTapTickButton:nil];

        [[[vc.upRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.upAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.upGreen backgroundColor] should] equal:[UIColor blackColor]];

        [[[vc.downRed backgroundColor] should] equal:[UIColor redColor]];
        [[[vc.downAmber backgroundColor] should] equal:[UIColor blackColor]];
        [[[vc.downGreen backgroundColor] should] equal:[UIColor blackColor]];

    }

});

There’s an element of stress testing in this last test - the sequence is repeated 25 times, which should be enough to expose any edge cases when wrapping back to the start of the state machine sequence. And the point about automated testing here is that you could equally choose to test the same sequence 250, 2,500 or 250,000 times - something that would be virtually impossible with manual testing.

Summary

This is a very simple app with a minimum of moving parts - yet it’s got all the elements needed to make for a complex set of interactions. Functions like locking some buttons in response to tapping others can quickly become convoluted and difficult to test thoroughly with a “trained monkey” approach.

Having a set of automated tests can help by allowing the tests to be exhaustive and completely repeatable. This would come into its own if the app was extended in the future - the original set of tests would immediately expose any areas where new functions broke old ones.

The other big advantage of taking a test-driven approach in my opinion is that it forces you to stop and think about the structure of the app at the right moment in the app’s lifecycle. By testing each element in isolation, it’s possible to be sure that one part works before moving onto the next one.

Although that might not be such a big issue in a “toy” app like this, on larger-scale projects (and particularly ones with multi-developer teams), testing will provide a “comfort blanket” that things work in the way which they’re intended to. That’s preventing a cognitive load that comes with uncertainty about the behaviour of areas of code.

Further reading

There’s no shortage of material about testing online and in books, but much of it suffers from the twin problems of a) approaching testing in a quasi-religious dogmatic “test all the things” approach; and b) being very Java-centric.

Kiwi as a framework is heavily influenced by RSpec, and the canonical reference for this is The RSpec Book by David Chelimsky. Although this is Ruby-centric, it’s a good introduction to the processes involved in behaviour and test-driven development.

Rails 4 In Action builds on the concepts covered in The RSpec Book to build out a working Rails site. Again, this is completely focussed on Ruby and Rails, but it does illustrate the BDD and TDD process extremely well.

For an iOS and Objective-C focussed approach, Graham Lee’s Test-Driven iOS Development is excellent. It doesn’t use Kiwi, but is a great introduction to the concepts of testing and practice using SenTest library.

There are relatively-few Kiwi-specific resources around - the GitHub wiki is increasingly comprehensive; and Test Driving iOS Development with Kiwi is a short iBooks title that covers the basics.

And of course, for all other questions and queries there’s the incomparable resource that is Stack Overflow.