Saturday, September 10, 2011

Unit Tests II

This series discusses some approaches to unit testing HelloWorld. in this article, we factor out a simple model object for the HelloWorld app.


Refacoring
In Unit Tests I, we generated some skeleton Logic and Application Tests for HelloWorld.  Before jumping into the implementation though, a minor refactoring will be worthwhile. We shall implement a very simple model class for our app.

If you have completed the iOS Tutorial from the earlier Hello World post, you should have a class named HelloWorldViewController which contains a userName property and a changeGreeting method looking something like this:
@property (nonatomic, copy) NSString *userName;
...

- (IBAction)changeGreeting:(id)sender {
self.userName = self.textField.text;

NSString *nameString = self.userName;
...
}
As you can see, the model object for the user data in our application is a simple NSString. This works fine for now, but the code will be more flexible if we have a separate model class which encapsulates this information. Let's call it HelloWorldModel:

@interface HelloWorldModel : NSObject
@property (nonatomic, copy) NSString *userName;
- (id)initWithName:(NSString *)name;
@end

@implementation HelloWorldModel
@synthesize userName;
- (id)initWithName:(NSString *)name {
self = [super init];
if (self) {
[self setUserName:name];
}
return self;
}
@end
As you can see, we have basically just wrapped userName in another class. For convenience, there is also a constructor (initWithName) which takes a name as an argument.

Next is to hook this model back into our app. This means replacing the userName field of the HelloWorldViewController class with a field of our own model type, and updating the changeGreeting method accordingly:

@property (nonatomic, copy) NSString *userName;
@property (strong, nonatomic) HelloWorldModel *model;
...

- (IBAction)changeGreeting:(id)sender {
self.userName = self.textField.text;
NSString *userInput = [self.textField.text];
self.model = [[HelloWorldModel alloc] initWithName:userInput];

NSString *nameString = self.userName;
NSString *nameString = [self.model userName];
...
}
(Deleted lines are shown in strikethrough.) Make sure to build and run the app to make sure it works as expected before proceeding.

Finally, let's establish some best practices early regarding memory management.  In the short term, this probably won't be an issue because HelloWorld only has one view.  However, as the application grows and more views are added, we want to make sure we clean up after ourselves if those views are unloaded. 

In the event of a low-memory condition, iOS may decide to remove an app's unused views from memory.  As the documentation explainsthe app will be notified via the viewDidUnload method of UIViewController.  When this happensview controllers that store references to subviews and outlets should release those references, and also release any objects that were created to support the view.  

Let's look at the implementation of viewDidUnload in HelloWorldViewController:

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    
    // Added automatically in tutorial.
    [self setTextField:nil];
    [self setLabel:nil];
}
Notice that Xcode has already inserted some code to release references to the textField and label outlets.  This happened automatically while we were building the interface for HelloWorld.  What about our instance of HelloWorldModel?  It won't be needed if the view is not visible, so let's release that too.  Add this snippet to viewDidUnload:
    [self setTextField:nil];
Yay!  Now we shall implement a unit test for this class.