Sunday, March 4, 2012

How to test storyboard iOS view Controllers.

I’m building an IOS 5.0 storyboard based application. As I began to write my controllers, I wanted a way to test things like:

1. You type this on this text box
2. You click on save.
3. Assert that the thing is saved on database.

I used to solve this kind of problem by instantiating testFields manually during the test and setting on the controller. However, when I tried using UITextView, I found out that I couldn’t instantiate an UITextView manually because  it should be instantiated on the main thread.

So I began to google for it, and at first it wasn’t promising. People kept telling me that to do that sort of testing I would have to use UIAutomation, but I was hoping to use UIAutomation to make tests that were more like system testing, and I really wanted to do some white box testing, like checking the database, which would be difficult or even impossible with UIAutomation.

So after a few more searches, I came across this blog: http://blog.carbonfive.com/2010/03/10/testing-view-controllers/. And it is good, because now I’ve seen how I can get a viewController with all the views already instantiated, which both simplified my tests and gave me hope that I could solve my problem.

However, this is how the author of the article instantiate a view controller:

    TestableSimpleViewController *viewController = [[TestableSimpleViewController alloc] initWithNibName:@"TestableSimpleViewController" bundle:nil];

This was bad for me, because in storyboards applications, the .nib file is encapsulated inside a .storyboard file, and I didn’t want to break this encapsulation nor did I want to recreate the nib files. So it was time to hit the apple documentation (which, fortunately, is very good). In this post I will describe the solution I came up with.

First, the project
I’ve came up with a one screen view project as a demonstration. It is a sample application where the user has two text boxes, and he types something on the first one, hit the button that says Copy Text, and the text is reproduced on the second text field.

This is the screenshot of my storyboard.



The first thing I did was create another target, called UnitTests, where I would put my tests. I then setup this second target to use GHUnit as explained in the GHUnit documentation located at http://gabriel.github.com/gh-unit/docs/appledoc_include/guide_install_ios_4.html. (The gunit documentation is a little outdated, but it is not difficult to adapt it to ios 5. You can use OCUnit also, but this tutorial will be demonstrated using GUnit).

Also make sure that both the iPhone.storyboard and all classes involved on the testing are also included in the UnitTests target. The way you can do this is clicking on each class, selecting the File Inspector, and making sure in the Target Membership view that UnitTests is selected for each file.

The last thing I gotta show about the project is the code. This are the contents of ViewController.h (my only ViewController class):


@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *originalTextField;
@property (weak, nonatomic) IBOutlet UITextField *resultTextField;
- (IBAction)copyText:(id)sender;

@end

I have to outlets, one for each textField, and also an IBAction. The important code is the copyText function, which is replaced below:

- (IBAction)copyText:(id)sender {
    self.resultTextField.text = self.originalTextField.text;
}

So now that I have everything in place, let’s test this.

Testing Setup
So, for this demonstration I will make one simple test that checks that if I click on the save text button, the second text box will be filled with the contents of the first box.

The first thing I gotta do is get the controller reference. To do this, I use the UIStoryboard object.

@interface ViewControllerTest : GHTestCase

@property (strong, nonatomic) ViewController *controller;

@end


@implementation ViewControllerTest

@synthesize controller = _controller;


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

-(void) testCopyText {
   
   
}

@end


In the setUp method I instantiated a storyboard with a name that is equal to the name of the storyboard bundle without the .storyboard extension.

Second, I instantiated a controller from the storyboard. In order to do this, I had to give the name of the controller (in this case, Bob).

So how do the storyboard now which of my controllers has the name Bob. Well, I had to tell it, of course. The way I do this is:
1. edit the storyboard file
2. select the view controller that I want to name
click on the attributes Inspector
3. On the View Controller section, that is a TextField labeled Identifier. Type Bob there.




The last thing I had to do is call a load view in the main thread. And now my controller is ready to be used.


Writing the test

Now, what’s missing is implementing the testCopyText function.

-(void) testCopyText {
    [self.controller.originalTextField performSelectorOnMainThread:@selector(setText:) withObject:@"foo" waitUntilDone:YES];
    [self.controller copyText:nil];
    GHAssertEqualStrings(@"foo", self.controller.resultTextField.text, nil);
   
}

The first I thing I did was set the text on the originalTextField. However, as this text can only be set on the main thread (since I’m interfering with the view), I used the prformSelectorOnMainThread method to do it.

The rest is pretty straightforward. I call the copyText: method and verify that the second resultTextField was set correctly.

Source code
You can find the source code at github. 

5 comments:

  1. Very good. I searched a lot for a example like this.

    ReplyDelete
  2. Thanks for the example. However when I do:
    [self.controller performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];

    I keep getting: NSInvalidArgumentException "-[__NSCFType pointSize]: unrecognized selector sent to instance 0x21f4690"


    ReplyDelete
  3. Thanks for sharing this. I've not been using Storyboards until recently, and this was helpful.

    ReplyDelete
  4. Hi,

    I really appreciate this blog, but can I see that storyboard view which I am testing on the simulator. Is it possible to see the button click which you're testing here in the example.

    Thanks

    ReplyDelete
  5. In this collection we have pick up best agriculture wordpress themes will help you present your family farming business at its best.

    ReplyDelete