Beginning Automated Testing With Xcode Part 2/2

Note from Ray: This is the tenth and final iOS 6 tutorial in the iOS 6 Feast! This tutorial comes from our new book iOS 6 By Tutorials. Charlie Fulton wrote this chapter – a friend of mine and one of the newest members of the Tutorial Team. Enjoy! This is a blog post by […] By Charlie Fulton.

Leave a rating/review
Save for later
Share

Learn how to add automated testing into your iOS 6 apps!

Note from Ray: This is the tenth and final iOS 6 tutorial in the iOS 6 Feast! This tutorial comes from our new book iOS 6 By Tutorials. Charlie Fulton wrote this chapter – a friend of mine and one of the newest members of the Tutorial Team. Enjoy!

This is a blog post by iOS Tutorial Team member Charlie Fulton, a full time iOS developer who enjoys hunting, fishing, and hanging out with his family.

Welcome back to our beginning automated testing with Xcode tutorial series!

In the first part of the tutorial, you learned how to add your code to Github, set up a Jenkins continuous integration server, and how to add a unit test to your app.

In this second and final part of the series, you’ll learn a lot more about unit testing, and how to automatically archive your builds and upload them to TestFlight!

Creating a unit test class

Now let’s add a simple unit test class for testing the WowUtils class. WowUtils looks up the string values for the Character class, Character race, and Item quality from the web service JSON data.

It’s best to create a new unit test class for each new class you want to test. Xcode has an Objective-C test case class template just for this purpose.

In the Xcode project navigator, right-click on the GuildBrowserLogicTests group, choose New File\Cocoa Touch\Objective-C test case class, click Next, enter WowUtilsTests for the class name, click Next again, make sure only the GuildBrowserLogicTests target is selected, and then click Create.

Now you have a test suite to which you can add some test cases. Let’s create your first test case. This test case will make sure that you get the correct response when looking up a character’s class.
Here are the methods you are testing from WowUtils.h:

+(NSString *)classFromCharacterType:(CharacterClassType)type;
+(NSString *)raceFromRaceType:(CharacterRaceType)type;
+(NSString *)qualityFromQualityType:(ItemQuality)quality;

Note:These methods are expected to get the correct name based on the data retrieved from Blizzard’s web service. Below you can see the output used by the WowUtils class (Chrome is a great tool for inspecting JSON output from a web service):

Replace the contents of WowUtilsTests.m with:

#import "WowUtilsTests.h"
#import "WowUtils.h"

@implementation WowUtilsTests

// 1
-(void)testCharacterClassNameLookup
{
    // 2
    STAssertEqualObjects(@"Warrior",
                         [WoWUtils classFromCharacterType:1],
                         @"ClassType should be Warrior");
    // 3
    STAssertFalse([@"Mage" isEqualToString:[WoWUtils classFromCharacterType:2]],
                  nil);
    
    // 4
    STAssertTrue([@"Paladin" isEqualToString:[WoWUtils classFromCharacterType:2]],
                 nil);
    // add the rest as an exercise
}

- (void)testRaceTypeLookup
{
    STAssertEqualObjects(@"Human", [WoWUtils raceFromRaceType:1], nil);
    STAssertEqualObjects(@"Orc", [WoWUtils raceFromRaceType:2], nil);
    STAssertFalse([@"Night Elf" isEqualToString:[WoWUtils raceFromRaceType:45]],nil);
    // add the rest as an exercise
}

- (void)testQualityLookup
{
    STAssertEquals(@"Grey", [WoWUtils qualityFromQualityType:1], nil);
    STAssertFalse([@"Purple" isEqualToString:[WoWUtils qualityFromQualityType:10]],nil);
    // add the rest as an exercise
}

@end

Let’s break this down bit-by-bit.

It’s always good to include a “failing test” in your test case. This is a test where the result is expected to fail. Again, you are using one of the assertion macros from the SenTestingKit – this time STAssertFalse.

The expected result, “Mage”, is compared to the result from the WowUtils method; if the test fails, you use the default message, since you passed in nil in this example.

  1. As I stated earlier, all test cases must start with the name test.
  2. The expectation is that the WowUtils class will give you the correct class name, given an ID. The way you verify this expectation is with the STAssert* macros. Here you are using the STAssertEqualObjects macro.
    The expected result, “Warrior”, is compared to the result from the WowUtils method; if the test fails, you log the message “ClassType should be Warrior”.
  3. Finally, you have another example test macro to use.
Note: For a complete list of the testing macros, check out the Unit-Test Result Macro Reference (http://bit.ly/Tsi9ES) from the Apple Developer library.

Now you can run your new test suite that presently contains one test case. Go to Product\Test (⌘-U).

Doh! You’ll see a compile error:

Since you added a new target, you need to let it know about the classes you’re trying to test. Every target has its own set of source files available to it. You need to add the source files manually, since you’re running a logic test target without the bundle and test host set.

Now click on the GuildBrowserLogicTests target in the TARGETS section. You should see this:

Expand the disclosure arrow in the Compile Sources section, and click on the + button. In the popup window, choose the following classes: Character.m, Guild.m, Item.m, and WowUtils.m. Then click Add.

Now add the app target as a dependency so that it will get built before your test target runs. Expand the disclosure arrow in the Target Dependencies section, click the + button, choose the GuildBrowser app target and click Add.

  1. Switch to the project navigator – if it’s not open, use the View\Navigators\Show Project Navigator menu item (⌘-1).
  2. Click on the project root to bring up the Project and Targets editor in the main editor area.
  3. Select the Build Phases tab along the top of Xcode’s main editing pane.

When these steps are complete, you should see the following:

Now run Product\Test (⌘-U) and the test should succeed. You can see the output from all the tests by switching to the Log Navigator (go to View\Navigators\Show Log Navigator (⌘-7)).

You should see this when you click on the last build on the left sidebar:

Make sure to commit and push your latest changes to Github.

Testing a class with local JSON data

Let’s take a look at the Character class and add a test suite for it. Ideally, you would have added these tests as you were developing the class.

What if you wanted to test creating your Character class from local data? How would you do that? What if you wanted to share such data with each test case?

So far, you haven’t used two special methods that are available in your unit tests: setUp and tearDown. Every time you run your unit tests, each test case is invoked independently. Before each test case runs, the setUp method is called, and afterwards the tearDown method is called. This is how you can share code between each test case.

When building an app that relies on data from web services, I find it very helpful to create tests using data that matches the payload from the services. Quite often in a project, you will only be working on the client side and waiting for the services from another developer. You can agree on a format and can “stub” out the data in a JSON file. For this app, I downloaded some character and guild data to create tests for the model classes so I could flesh those out before working on the networking code.

First add your test data. Extract this AutoTestData.zip file and drag the resulting folder into your Xcode project. Make sure that Copy items into destination group’s folder (if needed) is checked, Create groups for any added folders is selected, and that the GuildBrowserLogicTests target is checked, and then click Finish.

Update 10/31/12: There were some questions on the forums about the exact command line instructions to use to set up Jenkins, so if you have any troubles check out this handy list of Jenkins shell steps text file I put together for you.

Now add an Objective-C test case class for your Character test cases. In the Xcode project navigator, right-click on the GuildBrowserLogicTests group, choose New File\Cocoa Touch\Objective-C test case class, click Next, enter CharacterTests for the class name, click Next again, make sure only the GuildBrowserLogicTests target is selected, and then click Create.

Replace the contents of CharacterTests.m with:

#import "CharacterTests.h"
#import "Character.h"
#import "Item.h"

@implementation CharacterTests
{
    // 1
    NSDictionary *_characterDetailJson;
}

// 2
-(void)setUp
{
    // 3
    NSURL *dataServiceURL = [[NSBundle bundleForClass:self.class]
                             URLForResource:@"character" withExtension:@"json"];
    
    // 4
    NSData *sampleData = [NSData dataWithContentsOfURL:dataServiceURL];
    NSError *error;
    
    // 5
    id json = [NSJSONSerialization JSONObjectWithData:sampleData
                                              options:kNilOptions
                                                error:&error];
    STAssertNotNil(json, @"invalid test data");
    
   
    _characterDetailJson = json;
}

-(void)tearDown
{
    // 6
    _characterDetailJson = nil;
}

@end

Hammer time, break it down! :]

  1. Remember that test classes can have instance variables, just like any other Objective-C class. Here you create the instance variable _characterDetailJson to store your sample JSON data.
  2. Remember that setUp is called before each test case. This is useful because you only have to code up the loading once, and can manipulate this data however you wish in each test case.
  3. To correctly load the data file, remember this is running as a test bundle. You need to send self.class to the NSBundle method for finding bundled resources.
  4. Create NSData from the loaded resource.
  5. Now create the JSON data and store it in your instance variable.
  6. Remember that tearDown is called after each test case. This is a great spot to clean up.

Make sure everything is loading up correctly by running Product\Test (⌘-U). OK, now you have your test class set up to load some sample data.

After the tearDown method, add the following code:

// 1
- (void)testCreateCharacterFromDetailJson
{
    // 2
    Character *testGuy1 = [[Character alloc] initWithCharacterDetailData:_characterDetailJson];
    STAssertNotNil(testGuy1, @"Could not create character from detail json");
    
    // 3
    Character *testGuy2 = [[Character alloc] initWithCharacterDetailData:nil];
    STAssertNotNil(testGuy2, @"Could not create character from nil data");
}

Break it down!

  1. Here you are creating test cases for the Character class designated initializer method, which takes an NSDictionary from the JSON data and sets up the properties in the class. This might seem trivial, but remember that when developing the app, it’s best to add the tests while you are incrementally developing the class.
  2. Here you are just validating that initWithCharacterDetailData does indeed return something, using another STAssert macro to make sure it’s not nil.
  3. This one is more of a negative test, verifying that you still got a Character back even though you passed in a nil NSDictionary of data.

Run Product\Test (⌘-U) to make sure your tests are still passing!

After the testCreateCharacterFromDetailJson method in CharacterTests.m, add the following:

// 1
-(void)testCreateCharacterFromDetailJsonProps
{
    STAssertEqualObjects(_testGuy.thumbnail, @"borean-tundra/171/40508075-avatar.jpg", @"thumbnail url is wrong");
    STAssertEqualObjects(_testGuy.name, @"Hagrel", @"name is wrong");
    STAssertEqualObjects(_testGuy.battleGroup, @"Emberstorm", @"battlegroup is wrong");
    STAssertEqualObjects(_testGuy.realm, @"Borean Tundra", @"realm is wrong");
    STAssertEqualObjects(_testGuy.achievementPoints, @3130, @"achievement points is wrong");
    STAssertEqualObjects(_testGuy.level,@85, @"level is wrong");

    STAssertEqualObjects(_testGuy.classType, @"Warrior", @"class type is wrong");
    STAssertEqualObjects(_testGuy.race, @"Human", @"race is wrong");
    STAssertEqualObjects(_testGuy.gender, @"Male", @"gener is wrong");
    STAssertEqualObjects(_testGuy.averageItemLevel, @379, @"avg item level is wrong");
    STAssertEqualObjects(_testGuy.averageItemLevelEquipped, @355, @"avg item level is wrong");
}

// 2
-(void)testCreateCharacterFromDetailJsonValidateItems
{
    STAssertEqualObjects(_testGuy.neckItem.name,@"Stoneheart Choker", @"name is wrong");
    STAssertEqualObjects(_testGuy.wristItem.name,@"Vicious Pyrium Bracers", @"name is wrong");
    STAssertEqualObjects(_testGuy.waistItem.name,@"Girdle of the Queen's Champion", @"name is wrong");
    STAssertEqualObjects(_testGuy.handsItem.name,@"Time Strand Gauntlets", @"name is wrong");
    STAssertEqualObjects(_testGuy.shoulderItem.name,@"Temporal Pauldrons", @"name is wrong");
    STAssertEqualObjects(_testGuy.chestItem.name,@"Ruthless Gladiator's Plate Chestpiece", @"name is wrong");
    STAssertEqualObjects(_testGuy.fingerItem1.name,@"Thrall's Gratitude", @"name is wrong");
    STAssertEqualObjects(_testGuy.fingerItem2.name,@"Breathstealer Band", @"name is wrong");
    STAssertEqualObjects(_testGuy.shirtItem.name,@"Black Swashbuckler's Shirt", @"name is wrong");
    STAssertEqualObjects(_testGuy.tabardItem.name,@"Tabard of the Wildhammer Clan", @"nname is wrong");
    STAssertEqualObjects(_testGuy.headItem.name,@"Vicious Pyrium Helm", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.backItem.name,@"Cloak of the Royal Protector", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.legsItem.name,@"Bloodhoof Legguards", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.feetItem.name,@"Treads of the Past", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.mainHandItem.name,@"Axe of the Tauren Chieftains", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.offHandItem.name,nil, @"offhand should be nil");
    STAssertEqualObjects(_testGuy.trinketItem1.name,@"Rosary of Light", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.trinketItem2.name,@"Bone-Link Fetish", @"neck name is wrong");
    STAssertEqualObjects(_testGuy.rangedItem.name,@"Ironfeather Longbow", @"neck name is wrong");
}

You need to make sure that the properties correctly match up with the JSON data that you loaded initially. Just a few comments on what’s going on here:

  1. This tests the information shown in the main screen Character cell.
  2. This tests the information shown in the CharacterDetailViewController when you click on a Character cell from the main screen.

To have a little fun, “forget on purpose” to run Product\Test (⌘-U), and commit and push your changes. You will see what your little friend Jenkins will find, when you update your Jenkins job script to include the tests.

Charlie Fulton

Contributors

Charlie Fulton

Author

Over 300 content creators. Join our team.