Saturday, January 16, 2010

BDD for the iPhone

At Codemash this year several of my friends were exposed to iPhone development for the first time. I thought this would be a good time for me to share my experience driving iPhone application development with tests. This entry will walk you through getting xcode setup and your project linking against all of the libraries you will need to write effective tests.

With each new xcode release Apple has steadily improved support for Unit Testing. This is a good thing but it also means that the setup I will explain here is only guaranteed to work in xcode version 3.2.1. If you are using an order version you will likely need to make a few tweeks to the instructions below in order to get it working.

Adding Unit Test Support
The first thing we have to do after creating the empty project is add support for testing. The first step is to add a new build target.
  1. Right click on the Targets folder and select Add -> New Target.
  2. Select Unit Test Bundle for Cocoa Touch.
  3. I like to name this target Specs.
  4. This will bring up the Target Info dialog but you can just close it for now.
The next step is the add the SenTestFramework to our project. This is the xUnit library for Objective-C.
  1. Right click on the Frameworks folder and select Add -> Existing Frameworks...
  2. Select the Add Other... button at the bottom of the dialog.
  3. You should select the SenTestingKit.framework directory found under the /Developer/Library/Frameworks directory.
Next we will need to create a folder to hold our test classes. You do this by right clicking on the project name above the Classes folder and select Add -> New Group. I also like to name this group Specs. And finally we need to temporarily change our build target to build our test target. You can do this by selecting the drop down box in the upper left corner of xcode and selecting our Specs target.

With these steps out of the way we are finally ready to create our first test class. Right click on our Specs group and select Add -> New File. Select Objective-C test case class under Cocoa Touch Class. Let’s name this first class SimpleSpec. Make sure you only add this class to our Spec target.


There is no need to have a separate header and implementation file for our unit tests so I combine them in order to keep things as simple as possible. Select all of the code (including the import statements) from the header file and copy them to the top of the implementation file. Also, remove all of the example tests that were included by default. You can now delete the header file. In a later blog post I will show you how to change the template so it generates the test classes without these examples and in the same file. When you are finished you implementation file should look like:

#import <SenTestingKit/SenTestingKit.h>

#import <UIKit/UIKit.h>



@interface SimpleSpec : SenTestCase {

}

@end


@implementation SimpleSpec


@end


We are now ready to write our first test method. Add the following method to your implementation:


- (void) testShouldExecuteTest {

STAssertTrue(1 + 1 == 2, nil );

}


Build the project by executing ⌘B. You will get very familiar with this key sequence. I think of this as my “run unit tests” key combination. If everything is setup correctly you should see Succeeded in the bottom right corner of the window. Let’s bring up the build result window by pressing ⌘B. I usually keep this window open at all times as it gives me clearer feedback on the status of my builds.



To make absolutely sure you have everything setup correctly change the 2 to 3 in our tests and build again. You should see a failure this time.



Congratulations. Change the code back and you should see the build complete successfully. We have working Unit Tests but there is still one task remaining before we can move on.


As we develop our application we should not be building the Spec target. Instead we should be building our application target and have the Spec target build as a dependency. Luckily this is very easy to do. Right click on the application target and select Get Info. Make sure you are on the General tab and select the plus (+) button under Direct Dependencies. Add the Specs target and close the dialog. Change the build target to your application target and build again. If everything is setup correctly this build should succeed. Next change the tests so it will fail and build again. If it does fail you are ready to fix the test and move on.

Hamcrest

I have always felt that the SenTest API negatively effected the expressiveness of my tests so I was very happy to learn that Hamcrest was ported to Objective-C. I highly recommend you use this framework for all of your Objective-C testing. The library is not available as a binary so you will have to build it yourself. Check out the source from svn -> http://code.google.com/p/hamcrest/source/checkout. Once the source is checked out open the OCHamcrest.xcodeproj project file located in the hamcrest-objectivec/Source directory and build a Release version of the project. This will compile the project and create an OCHamcrest.framework directory in the hamcrest-objectivec/Source/build/Release directory.


The next step is to create a Frameworks directory in the root of your project. This should be a physical directory and not a virtual folder created via xcode. Copy the OCHamcrest.framework directory to your newly created Frameworks directory. Once this is complete add it to the project as a Framework the same way you added SenTestingKit.framework above.


Now we need to modify our Specs target to include Hamcrest. Right click on your Spec target and select Add -> New Build Phase -> New Copy Files Build Phase. Select Products Directory for the Destination and close the dialog. Drag the new build step to the top the of the build chain. Next select the OCHamcrest.framework from the Frameworks folder and drag it to the Copy Files build step. Also drag the framework to the Link Binary With Libraries build step. Your Spec target should look like this:



Now we can try to write a new test using Hamcrest. In your SimpleSpec test class write the following test method:


- (void) testShouldUseHamcrest {

assertThat(@"Apple", is(@"Apple"));

assertThat(@"Apple", isNot(@"Google"));

assertThat(@"Cheezy loves his iPhone", containsString(@"Cheezy"));

}


You will also need to add the following at the top of the file after the existing import statements:


#define HC_SHORTHAND

#import <OCHamcrest/OCHamcrest.h>

Build your project. If everything is setup correctly you should see success. I suggest you read the tutorial as it has everything you need to get started using Hamcrest. You can find it here.


OCMock

As you develop your iPhone application you will constantly have the need to mock and stub the UI framework classes. The framework for this is OCMock. The way you get it into your project is by following the same steps you used for Hamcrest. Copy the OCMock.framework folder to your Frameworks directory under your project and add it to the Frameworks virtual folder in the project.


Drag the OCMock.framework framework down to the Copy Files build step as well as the Link Binary With Libraries build step. We are now ready to try to write our first test using mocks.


In the SimpleSpec test class write the following method:


- (void) testShouldUseOCMock {

id mockString = [OCMockObject mockForClass:[NSString class]];

[[[mockString stub] andReturn:@"cheezy"] lowercaseString];

assertThat([mockString lowercaseString], is(equalTo(@"cheezy")));

}



and add the following import statement:


#import <OCMock/OCMock.h>


That is all there is to it. You can find several tutorials on the OCMock page.


Finishing Touches

There still are several simple things we can do to make our tests more expressive. First of all I do not like the assert syntax. I much prefer the language of bdd. The good news is that this is very simple to change. Add the following to the top of your SimpleSpec class:


#define ensureThat assertThat


Now we can change our verification to:


ensureThat(@"Apple", is(@"Apple"));


Also we now have a lot of statements at the beginning of our test class. I move all of them into a single header file and just include it in each file. The contents of that file are:


#define HC_SHORTHAND

#import <OCHamcrest/OCHamcrest.h>

#import <OCMock/OCMock.h>


#define ensureThat assertThat


Finally I am working on a change that will allow me to name my test methods more in line with the rspec approach. A test method could be named itShouldDoSomething instead of testShouldDoSomething. Stay tuned for further updates on this change.


I hope you found this entry helpful.



4 comments: