Thursday, September 15, 2011

Unit Tests III

This series discusses some approaches to unit testing HelloWorld. In this article, we implement some simple Logic Tests and Application Tests for the HelloWorld app.

(Note: If you do not already have a 'Tests' group or target within your project, additional configuration will be required that is beyond the scope of this post. As a workaround, create a new project, and make sure 'Include Unit Tests' is checked during the setup process.)

Logic Tests

In Unit Tests II, we factored out a HelloWorldModel class. Let's expand upon this and write some Logic Tests for it. By convention, the Logic Tests for a class called XxxYyyZzz are named XxxYyyZzzTests. So, following the steps outlined in Unit Tests I, create a new Logic Test class named HelloWorldModelTests:




The new test class will contain a skeleton test method.

Generally speaking, the Logic Tests for a class comprise test cases for methods of that class. Tests are written for each individual method, verifying their behavior matches expectations. By convention, if a class has a method called doSomething, a test for that method will be named testDoSomething.

In our case, the HelloWorldModel has a method called initWithName, so our test case will be named testInitWithName. We want to confirm that the name passed to the initWithName constructor is used correctly:
- (void)testInitWithName
{
NSString * const name = @"Godzilla";
HelloWorldModel *model = [[HelloWorldModel alloc] initWithName:name];
STAssertEquals(name, [model userName], nil);
}
The accessors for the userName property can also be tested:

- (void)testSetUserName
{
NSString * const name = @"Mothra";
HelloWorldModel *model = [[HelloWorldModel alloc] init];
[model setUserName:name];
STAssertEquals(name, [model userName], nil);
}
Build and run the tests to make sure they pass. As more methods are added to HelloWorldModel, more test cases should be added to HelloWorldModelTests.

Application Tests
Now let's implement an Application Test for HelloWorld. Typically, Application Test classes have the suffix AppTests. Following the steps outlined in Unit Tests I, create a new Application Test class called HelloWorldAppTests:



As before, the new test class will contain a stub test method.

Generally speaking, Application Tests check the integration of the various classes, rather than focusing on individual methods as Logic Tests do. For UI-driven apps like those on the iPhone, we would like to confirm that specific user actions update the UI in well-defined ways.

Naming conventions for Application Tests are more flexible, but should still be descriptive and start with the phrase test. In the case of HelloWorld, one requirement is that entering a name and clicking the button updates the label text accordingly. Let's call the test case testButtonClickUpdatesLabel:
- (void)testButtonClickUpdatesLabel
{
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];

// Downcast so we may access HelloWorld properties.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;

// Simulate user entering name in text field.
NSString * const name = @"Ebirah";
UITextField *textField = [hwViewController textField];
[textField setText:name];

// Simulate button click (quick & dirty).
[hwViewController changeGreeting:nil];
// Now check the label value.
UILabel *label = [hwViewController label];
NSString *labelValue = [label text];
NSString *expectedValue = [NSString stringWithFormat:@"Hello, %@!", name];
STAssertEqualObjects(labelValue, expectedValue, nil);
}
This test is slightly more verbose but still quite straightforward. The trick is to obtain a reference to the app's UIViewController so that one can then get references to the individual components of the UI, and manipulate them programmatically. Build and run the tests. As written, they should pass fine.

There is still room for improvement. (For example, instead of the "quick & dirty" code used in the test to simulate the button click, we could get a reference to the button itself and fake a "touch event".)* However, there is one glaring problem that needs to be addressed first. What will happen as we add more test cases to HelloWorldAppTests?

*[uiButton sendActionsForControlEvents: UIControlEventTouchUpInside];