Testing for cowards Part 2: Testing user interfaces

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

Introduction to part 2

This is the second of three posts (part 1 | part 3) 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 code and tests can be cloned or downloaded from GitHub: https://github.com/timd/TrafficLightTests.

Setting up the user interface for testing

Once the model works, I switch attention to the user interface (specifically, the user interactions). This is where the perception of iOS testing as difficult often arises - how do you test something that relies on a real live user touching something?

The answer is to think of the interaction as involving two layers - there’s the view layer, which the user touches; and the view controller which reacts to the touches with code. So you’re not interested in testing the touch itself - what you’re actually testing is the IBAction method behind the scenes

If you’re happy to assume that the UI controls are actually linked to the underlying method (and you can test that the connections are made correctly if you want to) then the testing process is simply a case of making sure that your didTapSomeButton method does what it should do when the someButton gets tapped.

The other UI testing question that crops up is how you can check the status of interface controls - what colour is the background of a UIView for example?

The reason this becomes an issue is that IBOutlets are normally declared in the view controller’s implementation file; and are encapsulated away from the view of the test.

You could declare them in the header file, but that would break encapsulation and just feels a bit wrong - working code shouldn’t have to change for the sake of tests. The workaround is to declare all the private properties and methods that your test will need access to in a category on your view controller at the top of the test. So the top of the UITests file looks like:

#import "Kiwi.h"
#import "ViewController.h"
#import "AppDelegate.h"
#import "LightEngine.h"

@interface ViewController (UITests)
@property (nonatomic, weak) id delegate;
@property (weak, nonatomic) IBOutlet UIView *upRed;
@property (weak, nonatomic) IBOutlet UIView *upAmber;
@property (weak, nonatomic) IBOutlet UIView *upGreen;
@property (weak, nonatomic) IBOutlet UIView *downRed;
@property (weak, nonatomic) IBOutlet UIView *downAmber;
@property (weak, nonatomic) IBOutlet UIView *downGreen;
@property (weak, nonatomic) IBOutlet UIImageView *stopImageview;
@property (weak, nonatomic) IBOutlet UIButton *startButton;
@property (weak, nonatomic) IBOutlet UIButton *tickButton;
@property (weak, nonatomic) IBOutlet UIButton *stopButton;
@property (nonatomic, strong) NSArray *upLights;
@property (nonatomic, strong) NSArray *downLights;
- (IBAction)didTapStartButton:(id)sender;
- (IBAction)didTapTickButton:(id)sender;
- (IBAction)didTapStopButton:(id)sender;
@end

SPEC_BEGIN(UITests)

// Tests start here

SPEC_END

Having gained access to all the outlets and methods for testing without needing to expose them to other classes, there’s one other step to make before the UI can be accessed by your tests.

A live view controller uses the initWithNibName:bundle: method to load the xib file, and loadView to trigger the three view creation methods - viewDidLoad, viewWillAppear: and viewDidAppear:

The loadView method won’t get triggered in a test - but you can force it by accessing the view property of the viewController with:

[myViewController view];

As soon as that property is accessed, the view lifecycle methods are triggered and all the outlets will be connected. The full code at the top of the test looks like:

 __block ViewController *vc = nil;

    beforeEach(^{
        vc = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
        [vc view];
    });

Testing delegates

The LightEngine object is connected to the view controller as a delegate, so testing that this is correctly wired up is an important step:

context(@"when instantiated", ^{

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

    it(@"should have the delegate set", ^{
        AppDelegate *appDelegate = [[AppDelegate alloc] init];
        [appDelegate application:nil didFinishLaunchingWithOptions:nil];
        [[(NSObject *)appDelegate.viewController.delegate should] conformToProtocol:@protocol(LightEngineProtocol)];
    });

});

This is pretty straight-forward - we instantiate an instance of appDelegate then fire the application:didFinishLaunchingWithOptions: method which is where the LightEngine is instantiated along with the view controller.

There’s one bit of Kiwi-specific wierdness - it’s necessary to get cast the appDelegate to an NSObject to get the test to compile for some reason that I’ve yet to fathom.

Testing the user interface

Assuming that you’ve got the user interface into a state where the tests can get at it, you can then start testing it. My approach is to start with the initial default state and test that everything is where I expect it to be - this can be useful if you want the UI to load with certain controls disabled, for example. In this case, I want the lights to be black rather than the default gray, and the buttons in the correct state:

context(@"when in the default state", ^{

    it(@"should show all lights as black", ^{
        [[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]];
    });

    it(@"should show the tick and stop buttons as greyed out", ^{
        [[theValue(vc.tickButton.alpha) should] equal:theValue(0.5f)];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(0.5f)];
    });

    it(@"should show the start button as active", ^{
        [[theValue(vc.startButton.alpha) should] equal:theValue(1.0f)];
    });

});

As you can see from the tests above, the view controller exposes the UIView properties and buttons, which makes testing their states very easy.

Once the default state is tested and working, then it’s a case of doing the same for each of the user interactions. First, the start button:

describe(@"and handling the start button", ^{

    it(@"should turn both red lights on in response to the start button", ^{
        [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]];
    });

    it(@"should lock the 'tick' button until the start button has been tapped", ^{
        [[theValue(vc.tickButton.enabled) should] equal:theValue(NO)];
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.enabled) should] equal:theValue(YES)];
    });

    it(@"should unlock the 'stop' button after the start button has been tapped", ^{
        [[theValue(vc.stopButton.enabled) should] equal:theValue(NO)];
        [vc didTapStartButton:nil];
        [[theValue(vc.stopButton.enabled) should] equal:theValue(YES)];
    });

    it(@"should show the stop and tick buttons as active after the start button has been tapped", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(1.0f)];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
    });

    it(@"should show the start button as inactive after the start button has been tapped", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.startButton.alpha) should] equal:theValue(0.5f)];
    });

});

When things get to the tick button, we can change approach slightly. The button causes a tick method to be sent to the delegate, which is an ideal place to use a mock object.

All we’re testing here is that the correct message is sent by the view controller - we’ve already tested what the delegate (in this case the LightEngine) will do. So there’s not really any point in going to the effort of creating a real, live LightEngine instance when we can use a mock object instead.

The mock will stand in for the LightEngine much as a stunt double will dive through the plate glass window instead of the movie star with the main billing. So long as the mock looks and behaves like a LightEngine, the view controller will happily accept it as such. This is often referred to as ‘duck typing’ - if it looks like a duck, walks like a duck and quacks like a duck, for our purposes it probably is a duck.

Here’s the code to do this:

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

    it(@"should send a 'tick' message to the delegate when the Tick button is tapped", ^{
        id delegateMock = [KWMock mockForProtocol:@protocol(LightEngineProtocol)];
        [[delegateMock should] conformToProtocol:@protocol(LightEngineProtocol)];
        [vc setDelegate:delegateMock];
        [[delegateMock should] receive:@selector(tick) andReturn:@164];
        [vc didTapTickButton:nil];
    });

    it(@"should show the tick button as active after each tick", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
        [vc didTapTickButton:nil];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
    });

});

Here we’re created an instance of the KWMock class and telling it to pretend to conform to the LightEngineProtocol. There’s a quick test to make sure that the mock has been created properly, and then the mock is set as the delegate of the view controller.

Then we set an expectation - that the delegateMock will receive a tick message, and we tell it to return @164 when it does. That expectation set, we can then call the didTapTickButton method. If the test gets to the end and the mock hasn’t received the tick message, the test will fail.

At this point, the combination of LightEngine and UI tests will have verified that all aspects of the tick process are working correctly. Next is handling the stop button:

describe(@"and handling the stop button", ^{

    beforeEach(^{
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.enabled) should] beTrue];
        [[theValue(vc.stopButton.enabled) should] beTrue];

        [vc didTapStopButton:nil];
    });

    it(@"should send the stopSequence message to the delegate when tapping the stop button", ^{
        id delegateMock = [KWMock mockForProtocol:@protocol(LightEngineProtocol)];
        [[delegateMock should] conformToProtocol:@protocol(LightEngineProtocol)];
        [vc setDelegate:delegateMock];
        [[delegateMock should] receive:@selector(stopSequence)];
        [vc didTapStopButton:nil];
    });

    it(@"should turn all lights black in response to the stop button", ^{
        [[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]];
    });

    it(@"should lock the tick and stop buttons after tapping the stop button", ^{
        [[theValue(vc.tickButton.enabled) should] beFalse];
        [[theValue(vc.stopButton.enabled) should] beFalse];
    });

    it(@"should unlock the start button after tapping the stop button", ^{
        [[theValue(vc.startButton.enabled) should] beTrue];
    });

    it(@"should show the tick and stop buttons as inactive", ^{
        [[theValue(vc.tickButton.alpha) should] equal:theValue(0.5f)];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(0.5f)];
    });

    it(@"should show the start button as active", ^{
        [[theValue(vc.startButton.alpha) should] equal:theValue(1.0f)];
    });

});

This set of tests is very similar to those which came before - the beforeEach block is where the stop button is tapped, and the tests are checking that the correct messages are sent; the lights change colour correctly, and the buttons get updated.

Testing the user interface

In the next post, I’ll cover testing the methods that update the user interface in response to the LightEngine codes.

http://adoptioncurve.net/archives/2013/09/testing-for-cowards-part-3-testing-the-full-interface/