UI Unit Testing in Your iOS Project

When you create a new project in Xcode, you’re given the option of including two types of tests: Unit Tests and UI Tests. But I’ve often wished that these two weren’t mutually exclusive.

There have been times when I’ve wanted to test UI components in isolation, but I wasn’t sure how feasible it was, or if it even made sense. A similar concept exists in some web frameworks (e.g. component integration tests in Ember or React), but I wanted to try it out in iOS.

The Problem

Unit tests and UI tests are basically at opposite ends of the spectrum when it comes to tests, in terms of both effort to write/debug/maintain and time to run. Integration tests fall somewhere in the middle. So it makes sense to me that unit tests should contain the most thorough test of any individual component. Integration and end-to-end tests then only need to be concerned with verifying the connections between components, without going into too much detail.

The problem with this is that, while it’s easy to isolate business logic into self-contained components for unit testing, it’s not as straightforward to do with UI, so we end up testing UI components as part of an end-to-end test. This requires a lot of setup that may be incidental to the interesting part of the test. Any relevant state needs to be faked, and the test must be instrumented to navigate to the appropriate place in the app.

Ideally, a UI unit test could instantiate the views it needs and proceed to test them. But in iOS, UI tests actually run separately from the host application, interacting only through the accessibility interface. This means they can interact with the app as a user would, but they can’t access the underlying objects.

Unit tests, on the other hand, can do pretty much whatever they want–including creating and interacting with UI. Since I’ve used the KIF framework successfully in the past, I decided to use that for my exploration.

Sidenote: I originally started this project in Swift, but switched to Objective-C after running into issues fighting Swift’s magic. I’m sure with some persistence, this approach could be applied to Swift as well.

Setup

While KIF was designed for UI testing, it’s actually installed as a unit test target. This means it can access all of the underlying objects driving your app, but we still need to take some steps to divert the default app startup sequence.

Thanks to this article, I discovered it’s possible to use a completely separate app delegate for testing.

Normally, the code in main.m simply launches the app and passes control to the app delegate. But with the following changes, it will first look for a TestingAppDelegate and fall back to the default AppDelegate:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Class appDelegateClass = NSClassFromString(@"TestingAppDelegate");
        if (!appDelegateClass)
            appDelegateClass = [AppDelegate class];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
    }
}

The important thing is that the TestingAppDelegate is added to the unit test target. This ensures it’s only available while running tests. My TestingAppDelegate is as simple as it gets, since it doesn’t have to do a single thing when the app starts. Even though we don’t create a window here, we’ll add a window property to use later:

#import <UIKit/UIKit.h>

@interface TestingAppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end
#import "TestingAppDelegate.h"

@implementation TestingAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Nothing to do here since even the window will be created by each test case!
    return YES;
}

@end

Now I just need to give my tests a way to create a window for themselves! An extension to KIF tester seems like a good place for this.

#import "KIFUITestActor+SpinPostAdditions.h"
#import "TestingAppDelegate.h"

@implementation KIFUITestActor (SpinPostAdditions)

- (void)makeWindowForViewController:(UIViewController *)viewController {
    UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    window.rootViewController = viewController;

    [window makeKeyAndVisible];

    TestingAppDelegate *appDelegate = (TestingAppDelegate *)[[UIApplication sharedApplication] delegate];
    appDelegate.window = window;
}

@end

This is pretty close to the way the default AppDelegate template works. Note that we are still assigning the window to the TestingAppDelegate. This ensures that only one window is in existence at any given time, and they don’t just keep stacking up as more tests are run.

Finally, everything is in place and I can write a test like this:

#import <XCTest/XCTest.h>
#import <KIF/KIF.h>
#import "KIFUITestActor+SpinPostAdditions.h"
#import "SayHelloViewModel.h"
#import "SayHelloViewController.h"

@interface SayHelloUITest : KIFTestCase
@property (nonatomic, strong) SayHelloViewModel *viewModel;
@end

@implementation SayHelloUITest

- (void)setUp {
    [super setUp];

    self.viewModel = [SayHelloViewModel new];
    SayHelloViewController *viewController = [[SayHelloViewController alloc] initWithViewModel:self.viewModel];

    [tester makeWindowForViewController:viewController];
}

- (void)testDefaultGreeting {
    [tester waitForViewWithAccessibilityLabel:@"What is your name?"];
    [tester enterText:@"Brian" intoViewWithAccessibilityLabel:@"Name"];
    [tester tapViewWithAccessibilityLabel:@"Say Hello" traits:UIAccessibilityTraitButton];
    [tester waitForViewWithAccessibilityLabel:@"Hello, Brian!"];
}

- (void)testAlternateGreeting {
    self.viewModel.greetingTemplate = @"Guten Tag %@!";

    [tester waitForViewWithAccessibilityLabel:@"What is your name?"];
    [tester enterText:@"Hans" intoViewWithAccessibilityLabel:@"Name"];
    [tester tapViewWithAccessibilityLabel:@"Say Hello" traits:UIAccessibilityTraitButton];
    [tester waitForViewWithAccessibilityLabel:@"Guten Tag Hans!"];
}

@end

SayHelloViewController is pretty basic, containing a label for a prompt, a text field, and a button (full source code is linked at the bottom of this post). Here’s what the test looks like in action:

After the initial startup, individual test cases run pretty quickly. Of course, this is a very simple example, and not all UI components lend themselves to testing this way. But I could also imagine using this approach while developing a new UI screen (especially when doing all or most layout in code), to interactively test or preview it.

Resources

References