Sunday, March 18, 2012

TDD on iOS - Part I.


I want to make a blog post about how to practice TDD on iPhone. My approach in this tutorial will be:

  • Build the prototype (the screen of the application)
  • For every functionality, first we will create the tests, then we will implement the code that make the tests pass.

What I won’t do in this post:
  • I won’t make a full application. I’ll try to illustrate how someone could use TDD to make a full application as an example, but I won’t try to do one.
  • I won’t worry about error conditions, like cases where a field in a form is obligatory, but the user decides not to fill it. However, from the examples that I do give, the approach to test these kind of conditions will hopefully become trivial.


The Prototype
I want to make for this example a birthday reminder application. It’s a very simple application that have the following user cases:
  • A user should be able to save the birthday date from his friends.
  • The user should be able to see all the birthday dates that he saved.

Based on these user cases, I created this prototype:



So, the user will see the list of people in a tableView. Every person will have on the cell his name and his birthday listed. The user will also be able to register new friends by clicking on the + button, typing the details for the friend, and clicking on the Save button.
The new registered friend should then appear on the table view.

I won’t save the data on the database for this example. It’s possible to do TDD in database oriented applications, and if you  want to know how I suggest you take a look at https://github.com/rafaeladson/intern-objc/blob/master/InternIOSTest/DataManagerBaseTest.m. Instead, I’ll use an array shared in the application delegate to simplify the example.

I’ll have two controllers: PeopleTableViewController, which will list the people and their respective birthdays, and NewPersonViewController, which will manage the view where the user can save new People.

The first test
First I’ll make the setup of the first test. In this setup, I’ll try to get the view from the storyboard. I’ll use the same approach I did on  http://yetanotherdevelopersblog.blogspot.com.br/2012/03/how-to-test-storyboard-ios-view.html

Here’s the setup of the first test:

#import "NewPersonViewControllerTest.h"
#import "NewPersonViewController.h"

@interface NewPersonViewControllerTest()

@property (strong, nonatomic) NewPersonViewController *controller;

@end

@implementation NewPersonViewControllerTest

@synthesize controller = _controller;


-(void) setUp {
   
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard_iPhone" bundle:nil];
    self.controller = [storyboard instantiateViewControllerWithIdentifier:@"NewPersonViewController"];
    [self.controller performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];

}

-(void) testPreConditions {
    STAssertNotNil(self.controller, nil);
}


@end

When I try to run this test, it will fail with the following message:

error: testPreConditions (NewPersonViewControllerTest) failed: Storyboard () doesn't contain a view controller with identifier 'NewPersonViewController'

To fix it, I have to edit my storyboard file to define that this controller has the identifier called new person view controller, in the same way I explained in my storyboard testing tutorial. After I do this, the test passes.

Now, for the first test, I’ll fill in the two fields and click on save. If all goes well, the array should end up with one element.


-(void) testSaveANewPerson {
    [self.controller.nameTextField performSelectorOnMainThread:@selector(setText:) withObject:@"Mike" waitUntilDone:YES];
    [self.controller.birthdayTextField performSelectorOnMainThread:@selector(setText:) withObject:@"October 10th" waitUntilDone:YES];
   
    [self.controller onSaveAction:nil];
   
    AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
    NSArray *people = delegate.people;
    int peopleCount = [people count];
    STAssertEquals(1, peopleCount, nil);
   
    Person *personInArray = [people objectAtIndex:0];
    STAssertEqualObjects(@"Mike", personInArray.name, nil);
    STAssertEqualObjects(@"October 10th", personInArray.birthday, nil);
   
}

Also, when I was making this test, I created the infrastructure for the test to compile, defining the outlets for nameTextField and birthdayTextField, the IBAction for onSaveAction and the people array on the application  delegate. However, I haven’t done any implementation yet. When I try to run this test, I get the following errors:

file://localhost/Users/rafael/dev/learning/examples/TDD/TDDTests/NewPersonViewControllerTest.m: error: testSaveANewPerson (NewPersonViewControllerTest) failed: '1' should be equal to '0':

file://localhost/Users/rafael/dev/learning/examples/TDD/TDDTests/NewPersonViewControllerTest.m: error: testSaveANewPerson (NewPersonViewControllerTest) failed: '<18a57906>' should be equal to '<00000000>':

file://localhost/Users/rafael/dev/learning/examples/TDD/TDDTests/NewPersonViewControllerTest.m: error: testSaveANewPerson (NewPersonViewControllerTest) failed: '<28a57906>' should be equal to '<00000000>':


Ok, so now I now that my tests are failing because the array should have 0 elements and it’s not initialized, so I have trash in the variables. Let’s try to implement by first modifying the application delegate to initialize the people’s array:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.people = [[NSMutableArray alloc] init ];
    return YES;
}

Now, let’s change the onSaveAction to save the person:

- (IBAction)onSaveAction:(id)sender {
    Person *person = [[Person alloc] init];
    person.name = [self.nameTextField text];
    person.birthday = [self.birthdayTextField text];
   
    AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
    [delegate.people addObject:person];
    [self.navigationController popViewControllerAnimated:YES];
}



Now, the tests pass.

In the last line, I did a popViewController to assure that after saving a new person I would automatically go back to the previous screen. If I wanted to test this behaviour, I would propably do it in an acceptance test rather than in a integration test like this one.



Coming up next

This is getting long, so I’ll break in several posts. In the next post I’ll tell you how you can implement the tableview part of the test and how to test segue behaviour.








You can check the code for this post at github. I'll change this code when I implement the following blog posts, so it's possible that the code will be a little different than what you saw here, but the principle will be the same.


Till then!

No comments:

Post a Comment