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][1] and [tests for interfaces using Cocoa bindings][2]. 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!

[1]: https://eschatologist.net/blog/?p=10 “Unit testing Cocoa user interfaces: Target-Action”
[2]: https://eschatologist.net/blog/?p=12 “Unit testing Cocoa user interfaces: Cocoa Bindings”

4 Comments

  1. Dave Dribin
    July 5, 2009

    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.

    Reply
  2. Allan Odgaard
    May 25, 2010

    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?

    Reply
  3. Pradeep
    September 26, 2011

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

    Reply
  4. Soaurabh Kakkar
    August 21, 2014

    Hi Chris,
    I am totally new to Objective-C/Cocoa, and currently working on a project which needs Unit testing Cocoa user interfaces, could you please provide a sample project of “Unit testing Cocoa user interfaces: Target-Action” , “Unit testing Cocoa user interfaces: Cocoa Bindings” and “Unit testing Cocoa user interfaces: Use Check Methods”. Because the implemented classes are a bit abstract for me.It would be very helpful if you do so, because i have never followed TDD approach.
    Please also suggest some books for cocoa Desktop App Development.
    Thanks and Regards,
    Soaurabh Kakkar

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *