iOS Programming Recipe 18: Unit Testing With GHUnit & CocoaPods

This Recipe will cover using GHUnit to unit test the Weather Application we developed in Recipe 15: Building A Weather Application. GHUnit is an open source unit testing framework for Mac and iOS that (in my opinion) has capabilities superior to the unit testing capabilities built into Xcode. Many developers have there own philosophy on unit testing and this Recipe will not attempt to define another. We will not be defining unit testing best practices or even go into detail about writing particular tests. This Recipe is purely about getting started with GHUnit and how it can be used in the context of a real application. Oh and did I mention? GHUnit makes writing asynchronous unit tests a breeze!

Assumptions

  • This recipe uses the Weather Application created in Recipe 15: Building A Weather Application, it is highly recommended that you work through or at least familiarize yourself with that recipe before continuing on to this one.
  • Source code for the weather app can be downloaded via GitHub. The revision of the Weather App after Recipe 15 has been tagged recipe-15, make sure to start there.
  • This recipe also relies heavily on CocoaPods, it is recommended you watch NSScreencast’s video tutorial if you are not familiar with CocoaPods.

Getting Started

Download The Starting Application
  • First download the source code for our starting application, which happens to be the Weather App developed in Recipe 15.
  • Refer to Recipe 15¬†for details on setting up a Weather Underground developer account in order to obtain a personal API key. You will need this key in order to access Weather Underground’s services.
  • The Weather App was built using CocoaPods, so after getting the source code you will need to navigate to the project root directory (where the .xcodeproj file lives) and run the following command in terminal. Note this will not work if you have not yet installed CocoaPods, learn about doing so here.

1
$ pod install
  • This will add all of the necessary external dependencies the Weather App needs. It will also create an .xcworkspace file which you will need to open when accessing this project (not the .xcodeproj file).
  • Add your person API key for Weather Underground to WeatherAPIKey.h, then build & run the application. If everything has been done right you should now have a functioning weather application!

Adding GHUnit Using CocoaPods
  • One of the unfortunate aspects about using GHUnit in your project is just that, using it in your project! It is a pain to get everything setup (even though they do have great documentation). CocoaPods can help us by shielding us from a lot of the project configuration.
  • In your browser, navigate to Cocoapods.org. Select iOS and search for GHUnit. Click the little button to the right of GHUnitIOS to copy the pod definition to your clipboard.
  • Next, open Podfile located in the application root directory. We will use TextMate to open the file and then paste the GHUnit pod definition at the end of the file.

1
$ mate Podfile

1
2
3
4
platform :ios, '6.0'
pod 'AFNetworking',         '~> 1.1'
pod 'SVProgressHUD',        '~> 0.8.1'
pod 'GHUnitIOS',            '~> 0.5.6'
  • The Podfile above in shown with the GHUnit definition added. Once you have added GHUnit save and close the file, then run pod install again to pull GHUnit into your project.
Adding A Testing Target
  • In order to run unit tests we need to add another build target to our project (Weather.xcodeproj). To do this first open Weather.xcworkspace, then select the Weather project from the project navigator. There will be a + button in the lower left corner of the project editor with the title Add Target, select it and you should see the following result.

1

  • From iOS Application select Empty Project and then click next.

2

  • Set the product name to WeatherTests, ensure Use Core Data and Include Unit Tests are both unchecked, while Use Automatic Reference Counting remains checked. Finally, click finish. This added a new testing target to your application.
  • This also added a few files that we won’t need. In the image below I have expanded the WeatherTests folder in the project navigator that was just added for the new target. Remove and delete the highlighted files, which are
    • AppDelegate.h
    • AppDelegate.m
    • Default.png
    • Default@2x.png
    • Default~568h@2x.png

3

  • Now we also need to make sure our new WeatherTests target is using the proper Pods configuration file supplied by CocoaPods, otherwise the target won’t be able to see GHUnit or any of the other dependencies added using CocoaPods. First select the Weather project from the project navigator, then in the upper left of the project editor select the top level project Weather then select the Info as shown below.

5

  • Expand Configuration then under Debug select the Pods configuration for WeatherTests as shown above.
  • While in the project editor, select the WeatherTests target on the left, then select the Build Phases tab from the top. Expand Link Binary With Libraries, then click the + button in the lower left corner to add a library. Select libPods.a from the list under workspace as shown.

9

  • Finally, we need to update main.m located in the WeatherTests folder to use GHUnit. Open main.m (make sure it’s the main for WeatherTests not Weather) and alter to add the following.

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate");
    }
}
  • All we did was change the app delegate class name to GHUnitIOSAppDelegate which is the GHUnit app delegate name for iOS.
  • Now you are ready to build & run WeatherTests! Select the WeatherTests target in the top right & run it. If everything went according to plan you should now see the following GHUnit Test harness.

4

Added Unit Tests

Now that we have added GHUnit and a test target we need to add some tests! We will actually only write one test for this article, but you should get the idea.

  • First add a new class WeatherAPITest to your project, specifically the WeatherTests target as shown below. For now have it inherit from GHTest. It is worth noting that the class name needs to end in Test.

6

  • Once it has been created, you need to do something a little strange. Go ahead and delete WeatherAPITest.h, we will add the interface declaration directly in WeatherAPITest.m.
  • We are also missing a few necessary frameworks & files in our WeatherTests target, so let’s go ahead and add them. First let’s add some frameworks! To make it easy, start by showing the file inspector Option + CMD + 1. Now expand the frameworks folder in the project navigator and select the following frameworks one by one and ensuring they are members of both the Weather target and the WeatherTests target as shown below.
    • CoreLocation
    • MobileCoreServices
    • SystemConfiguration
    • UIKit
    • Foundation
    • CoreGraphics

8

  • Now do the same thing for the following source files making sure they are members of both targets.
    • Observation.m
    • WeatherClient.m
  • As a final step, go ahead and make the following changes to WeatherTests.pch in order to add a few frameworks.

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Availability.h>

#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <SystemConfiguration/SystemConfiguration.h>
    #import <MobileCoreServices/MobileCoreServices.h>
    #import <CoreLocation/CoreLocation.h>
#endif
Writing A Test
  • Open WeatherAPITest.m and add the following code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import "WeatherClient.h"
#import "GHUnit.h"

#define kNetworkTimeout 30.0f

@interface WeatherAPITest : GHAsyncTestCase

@end

@implementation WeatherAPITest

- (void)testObservation
{
    [self prepare];

    __block NSError     *responseError      = nil;
    __block Observation *resposeObservation = nil;

    WeatherClient *sharedClient = [WeatherClient sharedClient];

    // Known Location (San Francisco)
    CLLocation *knownLocation = [[CLLocation alloc] initWithLatitude:37.7750 longitude:122.4183];

    [sharedClient getCurrentWeatherObservationForLocation:knownLocation completion:^(Observation *observation, NSError *error) {

        responseError = error;
        resposeObservation = observation;

        [self notify:kGHUnitWaitStatusSuccess forSelector:_cmd];
    }];

    // Wait for the async activity to complete
    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:kNetworkTimeout];

    // Check for Observation Object &amp; Error
    GHAssertNil(responseError, @"");
    GHAssertNotNil(resposeObservation, @"Response Observation is nil");

    // Validate (some) data
    GHAssertNotNil(resposeObservation.location, @"Response Observation.location is nil");
    GHAssertNotNil(resposeObservation.timeString, @"Response Observation.timeString is nil");
    GHAssertNotNil(resposeObservation.temperatureDescription, @"Response Observation.temperatureDescription is nil");
}

@end
  • First we add the interface that was deleted when we removed WeatherAPITest.h. We also added a timeout define to specify how long to wait for a test to complete. We then changed the superclass to GHAsyncTestCase which will allow us to write an async test for testing async code such as interacting with web services.
  • Finally, we create a test called testObservation. Each method that is a test must start with the prefix test.
  • testObservation will make an async call out to Weather Underground using our weatherClient and validate the response. You can see from the code we do this by first calling [self prepare] which is necessary for GHUnit to properly setup the async test case. Then we create two __block variables, one to capture an error, and another to capture the response (in that case an Observation object). Then, using our weatherClient, we make the observation request using a known valid location (San Francisco) and supplying a completion block to capture the result and to notify GHUnit that the async operation has completed. After making the call we tell GHUnit to wait here until the async operation has completed or our timeout value has been reached. After the wait, we then check that there wasn’t an error and that the response was valid using a few handy GHUnit macros.
  • After you have written the test above, build & run WeatherTests. Your new test should now show up in the GHUnit test harness. Run it, if you have supplied a valid Weather Underground API key as stated at the beginning of this article the test should pass (if you haven’t made a mistake that is), otherwise the test will fail.

7

Final Thoughts

Writing Unit Tests can be boring and tedious, GHUnit is one tool that can help make this a more pleasant process and like it or not having unit tests for important parts of your application will help in a number of ways. It’s also worth mentioning writing async unit tests such as the one above with nothing but the built in Xcode tools can be quite brutal, but with GHUnit it is a breeze and you get that cool test harness for free!

The source code to this recipe is available on gitHub, go check it out!

As always, comments, questions, or concerns are appreciated.

Speak Your Mind

*

css.php
Privacy Policy