Skip to content

Unit testing Cocoa user interfaces: Use Check Methods

In the past, I’ve talked about ways to easily write unit tests for Cocoa applications, including tests for user interfaces using target-action and tests for interfaces using Cocoa bindings. There are some strategies you can apply to make writing tests for Cocoa code even easier, though. They’re just straightforward object-oriented programming, but sometimes we can forget that all the techniques you might use in your main code base can also be applied to our test code.

So here’s one trick that you can use in writing tests for Cocoa user interfaces, especially in ways that will make test-driven development easier.

Use a Common Base Class with Check Methods

The first, probably most important thing to do is use your own common base class for your tests. Don’t derive your tests directly from SenTestCase; instead, derive them from your own MyTestCase class that in turn derives from SenTestCase. This gives you a place to put all of the customization that’s appropriate for your project.

Sometimes you might need a series of assertions to verify some particular state. However, that series of assertions will be the same every time you need to verify that state. Or the assertions themselves aren’t very intention revealing so you always wind up putting a comment above them describing what they’re really doing.

Checking Target-Action

A simple example of this is checking a target-action connection in a user interface. Say you have a view controller that presents a collection of objects managed by an array controller. Its view has an Add button that should send -addObject: to the array controller. You might write a test for it like this:

- (void)testAddButtonSendsAddObjectToArrayController {
    STAssertEquals([viewController.addButton target], viewController.arrayController,
        @"The Add button should target the array controller.");
    STAssertEquals([viewController.addButton action], @selector(addObject:),
        @"The Add button should send the -addObject: action.");
}

That’s not too difficult to understand, but it could be made simpler — it could be done in a single assertion. You’d just write a method to check both the target and action at once and then use that method from your test, like this:

// in your test base class...

/*! Tells whether the control sends the action to the target. */
- (BOOL)checkControl:(NSControl *)control
         sendsAction:(SEL)action
            toTarget:(id)target
{
    return ([control action] == action)
        && ([control target] == target);
}

// in the tests specifying the view controller's behavior...

- (void)testAddButtonSendsAddObjectToArrayController {
    STAssertTrue([self checkControl:viewController.addButton
                        sendsAction:@selector(addObject:)
                           toTarget:viewController.arrayController],
        @"The Add button's action should send -addObject: to the array controller.");
}

That makes the intention behind the entire test a lot clearer, and it makes writing the test easier & safer since you can’t (say) forget to check either the target or the action.

It does lose a tiny bit of information: If the test fails, you’ll have to look at your xib file instead of the failure message to determine whether it’s because the target or the action isn’t set as you’ve specified. However, the trade-off in making the test easier to read and write is worth it here.

Checking Outlets

This is even worthwhile for single assertions, such as those you’d use to test that your outlets are connected in the first place. For example, you might initially write a test that your view controller is your table view’s delegate like this:

- (void)testViewControllerIsTableViewDelegate {
    STAssertEquals([viewController.tableView delegate], viewController,
        @"The table view's delegate should be the view controller.");
}

Rewriting it to be more intention-revealing with a simple check method would make it look like this:

// in your test base class...

/*! Tells whether the outlet is connected to the given destination. */
- (BOOL)checkOutlet:(id)outlet connectsTo:(id)destination {
    return outlet == destination;
}

// in the tests specifying the view controller's behavior...

- (void)testViewControllerIsTableViewDelegate {
    STAssertTrue([self checkOutlet:[viewController.tableView delegate]
                        connectsTo:viewController],
        @"The table view's delegate should be the view controller.");
}

You’re not saving any code by writing your test this way — you’re actually writing more — but its complexity has gone down because it requires less effort to see what it’s actually trying to do.

Checking Bindings

This is even worthwhile in situations where you may still need a few extra assertions. For example, Cocoa bindings are specified using a lot more information than just outlets and target-acton connections; you won’t always want to check (and specify the value of) all of it, but you can easily make the common parts clearer.

Going back to our Add button example, as is typical its enabled state should be bound to the array controller’s canAdd property. Writing a test to specify this involves using -infoForBinding: and interpreting the results, which takes a couple lines of code and a couple of assertions:

- (void)testAddButtonEnabledStateIsBoundToArrayControllerCanAdd {
    NSDictionary *bindingInfo = [viewController.addButton infoForBinding:NSEnabledBinding];
    STAssertNotNil(bindingInfo,
        @"The Add button's enabled state should be bound.");

    id observedObject = [bindingInfo objectForKey:NSObservedObjectKey];
    STAssertEquals(observedObject, viewController.arrayController,
        @"The Add button's enabled state should be bound to the array controller.");

    NSString *observedKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
    STAssertEqualObjects(observedKeyPath, @"canAdd",
        @"The Add button's enabled state should be bound through the 'canAdd' key path.");
}

This isn’t too complicated, but it does start to get tedious, especially given that you have to remember to distinguish between STAssertEquals (pointer equality) and STAssertEqualObjects (object equivalence). Let’s put the tedium in one place:

/*! Tells whether the object's binding is connected through the given key path. */
- (BOOL)checkObject:(id)source
         hasBinding:(NSString *)binding
           toObject:(id)destination
            through:(NSString *)keyPath
{
    NSDictionary *bindingInfo = ;
    id observedObject = [bindingInfo objectForKey:NSObservedObjectKey];
    NSString *observedKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];

    return (bindingInfo != nil)
        && (observedObject == destination)
        && [keyPath isEqualToString:observedKeyPath];
}

// in the tests specifying the view controller's behavior...

- (void)testAddButtonEnabledStateIsBoundToArrayControllerCanAdd {
    STAssertTrue([self checkObject:viewController.addButton
                        hasBinding:NSEnabledBinding
                          toObject:viewController.arrayController
                           through:@"canAdd"],
        @"The Add button's enabled state should be bound to the array controller's 'canAdd' property.");
}

Much clearer!

{ 3 } Comments

  1. Dave Dribin | July 5, 2009 at 8:46 pm | Permalink

    This looks very much along the lines of the custom assertion pattern:

    http://xunitpatterns.com/Custom%20Assertion.html

    A true assertion macro like DDAssertOutletConnectsTo() or DDAssertBinding() would provide better context information and better failure messages; however, creating true custom assertions in OCUnit is a bit of a pain due to the assertions being rather complicated macros. I’ve played around with a few ways to make it easier to create custom assertion macros, though one downside of using macros is you’d lose the readability of Objective-C’s interleaved arguments.

  2. Allan Odgaard | May 25, 2010 at 11:03 am | Permalink

    It seems to me that all these assertions should always hold (at run-time) and hence could just as well be placed in your window controller’s init or awakeFromNib method.

    Are you doing any interface tests which would not be true in a normal execution of the application?

  3. Pradeep | September 26, 2011 at 9:31 pm | Permalink

    how to write the test base class and access them in unit test class as [self test_bass_class_method].

Post a Comment

Your email is never published nor shared. Required fields are marked *