Previous Entry Add to Memories Share Next Entry
Unit testing Cocoa user interfaces: Target-Action
latest
chanson
It's really great to see that a lot of people are adopting unit testing for their projects and dramatically improving their quality. Test-driven development and agile development methodologies built around it are really taking off. However, a lot of people still feel that their user interface is difficult to test through code, and either requires a capture-playback tool or requires a different design approach based heavily on interfaces/protocols to get right.

In last year's post Trust, but verify. I tried to dispel some of the mystery of testing your application's user interface when using the Cocoa frameworks. However, I've still had a lot of (entirely well-justified!) requests for examples of how to put it into practice. So here's a simple example of what I'd do to write a unit test for a button in a window that's supposed to perform some action.

First, when implementing my window, I'd follow the standard Cocoa pattern of having a custom NSWindowController subclass to manage my window. This window controller will have an outlet connected to each of the views in the window, and will also wind up with a private accessor method — used only within the class and any subclasses, and in testing — for getting the value of each of its outlets. This design flows naturally from the test which I would write to specify that the window should contain a button. First, here's the skeleton into which I'd put tests:
// TestMyWindow.h

#import <SenTestingKit/SenTestingKit.h>

@class MyWindowController;

@interface TestMyWindow : SenTestCase {
    MyWindowController *_windowController;
    NSWindow *_window;
}
@end

// TestMyWindow.m

#import "TestMyWindow.h"
#import "MyWindowController_Private.h"

@implementation TestMyWindow

- (void)setUp {
    // MyWindowController knows its nib name and
    // invokes -initWithWindowNibName: in -init
    _windowController = [[MyWindowController alloc] init];

    // Load the window, but don't show it.
    _window = [_windowController window];
}

- (void)tearDown {
    [_windowController release];
    _window = nil; // owned by _windowController
}

@end
That's the infrastructure into which I'd put my other test methods for this window. For example, I'll want to specify the nib name for the window controller and ensure that it actually knows its window:
- (void)testNibName {
    STAssertEqualObjects([_windowController windowNibName], @"MyWindow",
      @"The nib for this window should be MyWindow.nib");
}

- (void)testWindowLoading {
    STAssertNotNil(_window,
      @"The window should be connected to the window controller.");
}
Now let's check that I have a "Do Something" button in the window, and that it sends an action directly to the window controller.
- (void)testDoSomethingButton {
    // _doSomethingButton is a private method that returns the button
    // conected to the doSomethingButton outlet
    NSButton *doSomethingButton = [_windowController _doSomethingButton];
    
    STAssertNotNil(doSomethingButton,
      @"The window should have a 'Do something' button.");
    
    STAssertEqualObjects([doSomethingButton title], @"Do Something",
      @"The button should be titled accordingly.");

    STAssertEquals([doSomethingButton action], @selector(doSomething:),
      @"The button should send -doSomething: to its target.");

    STAssertEquals([doSomethingButton target], _windowController,
      @"The button should send its action to the window controller.");
}
You'll notice something I'm not doing in the above: I'm not simulating interaction with the interface. This is the core of the trust, but verify approach to unit testing of your user interface.

I can trust that as long as I verify everything is hooked up properly that Cocoa will cause the button to send its action message to its target — whether it's a specific object or, if the target is nil, the responder chain — whenever the button is clicked while it's enabled and not hidden. I don't need to simulate a user event, and I don't even need to display the interface while running the unit tests. All I need to do is inspect, through code, that everything is wired up correctly.

Note that I can do way more than the above in testing my interface design, too. For example, I can ensure that the control layout is correct according to what my interface designer has specified, by checking bounding rectangles for example. But testing only the functionality of my interface has significant advantages, too. For example, it doesn't matter if I wind up using a custom kind of button to achieve exactly the kind of look and feel or behavior I need. It doesn't matter if I wind up changing the layout in response to feedback. No matter what I do, I'll know that functionality won't accidentally break while I'm messing around in Interface Builder — even if I completely rip out my interface and replace it with a new one!

This approach can also be used for testing Cocoa bindings using the -infoForBinding: method that was introduced in Mac OS X 10.4 Tiger. I hope to write up a post soon on how to approach Cocoa bindings using these same techniques, but it should be fairly straightforward given the above and the above documentation.

Update: I've struck through the check of the button's title above, because you may or may not want to do that. For example, if you're primarily running your unit tests against your development localization, you may want to put it in. But if you want to run your unit tests against a localized build of your application, you'll probably want to avoid checking a localized title against an English string. A "have your cake and eat it too" strategy might be to keep a variable somewhere in your application that can be used to selectively disable checks of only localized strings.

Update July 7, 2007: I've finally written a post, Unit testing Cocoa user interfaces: Cocoa bindings, on how to write tests for Cocoa bindings. Now there's no excuse for not doing test-driven development of your Cocoa user interfaces!

I guess I don't agree...

(Anonymous)

2006-07-04 05:00 am (UTC)

Regarding "-testNibName ", I think it'll be instantly obvious in if your nib name is wrong (eg, nothing will load), and you've now made it so you have to change your code in _two places_ if you change your nib name, instead of just one. This kind of duplication doesn't help development time.

Also, the runtime system will log a message if your button is not hooked up, so that verification seems like extra code to me. And checking the button's title just seems plain wrong -- what happens when your app is localized? Why assume the developers only speak English? Also, if you do change the button title in NIB, you now have to change your code. Why have everything twice? Why not just have two NIBs, and do a logical "AND" of them when you run, to make sure every change is really made? NIB seems just as valid as code to me. I don't need verification that my NIB is correct -- I run the app, and look at it, and it's verified. It doesn't need to be verified beyond that, programmatically.

I certainly understand wanting to test code, but this whole approach is not something I would pursue.

Yours,
-Wil Shipley

Re: I guess I don't agree...

(Anonymous)

2006-09-11 07:18 pm (UTC)

Totally agree with Wil Shipley's comment on this one.

In unit-testing the nib typically is loaded but not shown, so there is nothing obvious about the nib name being wrong or right. You are right that nothing will load, but the view controller will initialize just fine, with a generic view. For this reason one should use initWithNibNamed:bundle: when instantiating the view controller, btw.
As for verifying that connections are indeed connected by looking at them -- I'm sure it works fine for Superdeveloper but I would advice everyone else to run at least a notNil test.

Controller/Model instead of UI Tests

(Anonymous)

2006-07-05 07:44 am (UTC)

I'm with Wil that it makes not much sense to test the name of the nib or the label. But the fact that the example is a little bit basic does not mean that Target/Action and Bindings do not help a lot in testing UIs.

It will be more common that someone will call the target of a button and want to test if the outcome is correct (e.g. testing the value of an input field...).
With these kind of tests you are testing the functionality of the controller not the UI. A typical problem with this approach is that you test the model too and you have to provide test data or setup code to do that.
Therefore it's more a functional test than a unit test. A solution would be a special mock/testable model that is used for the tests.

Bindings can be really complex. Especially when there are dependencies between keys it's easy to oversee problems. Doing unit tests for these things can improve quality a lot as you probably will not test all variants on the UI by hand all the time. That's also not a test of the UI but more a test of the model objects. 

The main benefit of bindings and target/action is that they help to keep the UI, model and controller layer separate. This separation is the basis for good tests. Testing the model and the controller should suffice most of the time.

To find problems in the NIB files I would like that they get verified by the build process. A warning could be generated for elements that have no valid binding, outlet or target/action. An easy way to spot those elements in the interface builder could also help a lot I think. 
Similar is already done for Automator actions which get post processed and verified and add warnings. 

A warning/error from the build system must be treated as the first test that is going wrong.

best regards
  Martin Kahr  

Getting info from -infoForBinding:

(Anonymous)

2006-07-06 11:37 am (UTC)

I'm not sure -infoForBinding: returns enough info to debug bindings properly.

For one, there are no public keys for the dictionary returned, and (I have tried) it doesn't seem possible to get each value for every component of a binding (observer, key, etc.). Though I would love it if I'm wrong!

Ken.

Re: Getting info from -infoForBinding:

chanson

2006-08-19 04:30 am (UTC)

Actually, the keys for the dictionary returned by -infoForBinding: are public in Tiger. See NSKeyValueBinding.h in AppKit; the three public keys are NSObservedObjectKey, NSObservedKeyPathKey, and NSOptionsKey. There are also public constants for binding names and binding option keys.

I'm not sure what you mean by getting each value for every component. You can certainly find out what the observed (bound-to) object for a binding is, what key path is bound, and what the binding's options are using the above information.

Student Sunday Slides

(Anonymous)

2006-08-19 03:37 am (UTC)

Hi there,

Googled for test driven development to follow up from your talk on Student Sunday and I found your site, lots of great useful information but I would also like some material related to the talk you gave.

Any pointers to wherever I can get into this test driven development stuff would be appreciated.

Thanks,

Mathieu Tozer
email@mathieutozer.com

Re: Student Sunday Slides

chanson

2006-08-19 04:25 am (UTC)

Mathieu:

Thanks! I'm glad you found it useful. Some good resources are the books "Test-Driven Development" by Kent Beck and "Refactoring" by Martin Fowler, as well as "Working Effectively with Legacy Code" by Michael Feathers. There are also the extremeprogramming, testdrivendevelopment, refactoring, and testfirstuserinterfaces mailing lists at Yahoo, which are good resources where you can find a large number of like-minded individuals to help you - even some other Cocoa developers.

I won't be making any material related to my session available here (this is my personal blog). Your best bet is to contact Apple Developer Connection and ask them about getting material from the session available to attendees.

Hello! I'm new to Cocoa, and I'm trying to get my views under test (just like in your article!). Unfortunately I can't seem to figure out how you get your window instantiated.

I'm writing a Document based application with a subclass of NSDocument. I've successfully instantiated the NSDocument, called "makeWindowControllers", and grabbed my window controller. But for some reason, when I ask the window controller for the window, I get nil returned!

My test code is here:

http://github.com/tenderlove/MeetingTime2/blob/master/TestMyDocument.m

I would greatly appreciate any help you can give! My typical development style is TDD, and the TDD informational resources for Cocoa seem very limited. Thank you for your articles, they've gotten me much farther than I would on my own.

new emblematic is their architectural cloth. coach outlet, on the mismated of shopping centers, are repeatedly body elaborating owing to meagre towns, harbour streets, seats, alleys, and burberry outlets shops along the roads and streets. Their formulate is conspicuously disparate than informal malls, which are closed spaces, dissemble garish ragtime passage surface of the louis vuitton outlets speakers and neon mirrorlike limpid that could busted up somebody’s faculty. Lately several outlet stores postulate been developed according to coach outlets individualizing regional architectural themes, being exhibit some of them adopted a neoclassical burberry outlet charm fame Veneto, the land stage the important neoclassical sculptor Canova was born, or they admit been realized prolific by the Renaissance comeliness prestige Tuscany. These solutions induce the environment incalculably more louis vuitton outlet pleasant, inasmuch as is most further plush to hike withdrawn its alleys, dominion comparison to actualize spaces curtain no windows of characteristic shopping centers.

заклепка для одежды купить
брендовая детская одежда интернет магазин
r одежда
интернет магазин недорогой одежды украина
вешалки для одежды купить
футболки муж и жена
686 одежда купить
одежда селин би купить
одежда helly hansen купить
купить одежду через интернет костюм
кружка в марьино
футболка с совой
одежда акции
футболка на заказ москва
бейсболка diesel
reima детская одежда распродажа
майки в краснодаре
купить одежда богнер
магазин белой одежды
где можно купить рэперскую одежду
брендовая одежда и обувь купить
cropp town кепки
футболка сборной голландии
печать надписей на футболках
футболка фрак
архангельск магазины одежды
элитная детская одежда интернет магазин
футболка олимпиада
магазин одежды вайлдберриз
магазин одежды oliver
майка jack daniels
футболки с приколами для детей
купить нарядную одежду большой размер
купить одежду в кракове
прикольные футболки в питере
футболки гриффины
майки стрейч
интернет магазин одежды новосибирск
sum 41 футболки
работа в магазине одежды в спб
где в иваново купить футболку
stan футболки
футболка бэтмен купить
магазины одежды г москва
магазин одежды sela
одежда эмо
стиль 80 х в одежде
психоделические футболки
одежда для йоги
http://futbolka-ja-feja.co.tv/map.php

Die Seite von Euch ist super

(Anonymous)

2011-01-12 04:50 am (UTC)

Tolle Blogartikel die auf jedenfall auch allen gefallen dürfte. Aber auch meine Seite dürfte gefallen auch wenn sie sich um [url=http://www.sexakt.org][/b]Live Sex Cam[/b][/url] dreht, aber völlig private Girls zeigt.

The Ultimate MIND READING Trick. SUPERR!

(Anonymous)

2011-01-28 06:59 am (UTC)

Hi) i could not find the best category for my unusual post, so i'm sorry for (may be) inaccuracy) but i'm sure that i must write about my discovery..))

ONLY SEE what i had found several days ago http://bit.ly/revelation-effect

This site can show you the Ultimate Mind Reading Trick.
I was so surprised..
When i found out how easy it is, I was shocked. and now I'm really want that you try to perform this amazing trick, too)))
good luck
and Greetings from Spain)))

Hi,
I have taken three text fields, one for first name ,last name and third for getting full name along with that a button is also there. After entering first,and last name i press the button and full name should appear in the third text field. I dont know how to write unit test for this thing could any help me regarding this.