TaskRabbit is Hiring!

We’re a tight-knit team that’s passionate about building a solution that helps people by maximizing their time, talent and skills. We are actively hiring for our Engineering and Design teams. Click To Learn more

A Previous Employee

Testing iOS UI code with Kiwi

@ 14 Mar 2014

ios testing


Testing iOS UI code with Kiwi

At TaskRabbit, we use automated testing in iOS, Android, and web projects. When I first was talking to Julian about joining TaskRabbit, I got super excited when I heard they were testing the iPhone app. Testing wasn't so popular in the iOS community at the time (not that this has changed).

When I first started, the team was using Cedar as a testing framework. I had used Kiwi in past projects and was hooked. Kiwi is a treat to work with, is written purely Objective-C and is built on top of SenTestKit/XCTest - check it out here. We ended up switching to Kiwi to test the app and the result has been nothing but positive.

Testing iOS UI code isn't always so straightforward and requires special considerations, so I wanted to share some techniques that we are using to make testing more enjoyable.

Integration test using public interfaces

We originally tested some functionality in the app by exposing private methods and instance variables. With the intention of making testing easier, we added class continuations to create interfaces available to specs. At the end of the day, this made changing code and refactoring more difficult than it should have been. A good practice that we adopted is to write all specs using public interfaces.

For example, to test the result of a button being tapped, we emulate the tap event by sending the action directly to the button via sendActionsForControlEvents:. Following this pattern allows us to ruthlessly refactor without having to change the test suite.

Use a different application delegate

A UIApplicationDelegate implementation is designed for the normal application life-cycle. For specs, we use a different application delegate, which doesn't implement any application life-cycle methods. This avoids unwanted side effects that would be caused by the normal app delegate.

Use an in memory Core Data store

Using an in memory store saves us from creating and deleting a SQL-lite database for each spec. We access the Core Data stack's managed object context through a category on NSManagedObjectContext. This makes using a different stack straightforward. We initialize the main stack as an in memory store and any UI code that accesses the main context will use this store.

Add custom helpers to make life easy.

Kiwi doesn't have a global beforeAll and afterAll which I think is for the better. To simplify code, and avoid duplication, we have created functions that replace the default Kiwi beforeEach, afterEach, beforeAll, and afterAll blocks. This solution is better because we use specific functions to setup and teardown the exact conditions needed for each scenario.

We typically use the following helper to reset and initialize our Core Data stack before each test. The first call resets remote operations that are happening in the shared context and the second call sets up a new instance of the Core Data stack.

extern void CD_beforeEach(void (^block)(void));

void CD_beforeEach(void (^block)(void)) {
    beforeEach(^{
        [[NSManagedObjectContext context] cancelRemoteOperations];
        [TRCoreDataStack setupInMemory];
        if (block) block();
    });
}

Manually populate the model for each controllers suite

When testing view controller code, we sometimes need to populate our model with test data. We have had success manually populating the model in each test suite. In most specs, this is done in beforeEach blocks. It allows us to setup specific conditions and test edge cases in the UI easier than having a set of default models.

This is a typical example of a view controller setup block. The CD beforeEach call is a wrapper of the default beforeEach. The last function call adds the controller to the window. We don't always add the view controller's view to the window but sometimes this is required to test certain UIKit objects like UITableView.

__block TRUserProfileViewController *controller;
__block User *user;

CD_beforeEach(^{
    user = [User create];
    user.rating = 4.99;
    user.publicName = @"Evan Tahler";

    controller = [[TRUserProfileViewController alloc] initWithUser:user];
    setKeyWindowWithController(controller);
});

Adding the controller to a fresh window as a 1 liner.

extern void setKeyWindowWithController(UIViewController *controller);

void setKeyWindowWithController(UIViewController *controller) {
    UIWindow *window = [UIWindow new];
    window.rootViewController = controller;
    [window makeKeyAndVisible];
}

In controller code, we pass around the current context to fetch and create NSManagedObjects. For specs, we implemented create: to easily create objects in the main context.

    user = [User create];

It is also worth noting that the user is retained by the spec. This allows us to stub methods without exposing a public interface on the controller.

Mock out the backend with Kiwi

In view controller test code, we are setting up the model in each suite. This makes it easy to stub out networking requests that happen deep in the model layer. Typically, we will stub a method on the model, and execute its completion block with the results we want. We have had success using the Kiwi provided stubbing support stub:withBlock: or KWCaptureSpy. Testing this way keeps a layer of abstraction between the model and the controller.

it(@"should present an alert when the review fails to save", ^{
    [review stub:@selector(saveWithSuccess:failure:) withBlock:^id(NSArray *params) {
        TRVoidErrorBlock failure = params[1];
        NSError *error = [NSError nullMock];
        [error stub:@selector(trMessage) andReturn:@"Message"];
        failure(error);
        return nil;
    }];

    TRButtonContainerView *view = (id)controller.textView.inputAccessoryView;
    [view.button sendActionsForControlEvents:UIControlEventTouchUpInside];
    [[[UIAlertView currentAlertView].title should] equal:@"Message"];
});

Test what matters

We test a lot of UI code in our iOS apps. Some parts of the UI are better tested by a human - notably layout code, animations, fonts, and colors. We don't really see a benefit in automating this type of testing.

Comments

Coments Loading...