Quantcast
Channel: Fairway Technologies
Viewing all articles
Browse latest Browse all 53

iOS Unit Testing With OCMock

$
0
0

Mock objects are an important driver of test driven development (TDD). They give developers the ability to craft unit tests involving complex objects without risking the possible complications inherent in instantiating those objects. They can be used to test code’s behavior when things go wrong, when infrequent things happen, or when a complex system of objects needs to be in a specific state. They are good for testing methods that return non-deterministic results (like the current time), and standing in for objects you plan to build, but haven’t built yet. In short, they’re useful, but xCode does not support them out of the box.

Apple’s xCode ships with OCUnit which is “a faithful implementation of xUnit patterns for Objective-C”[Vollmer]. Though useful for testing (it provides the various combinations of assertions covering nulls, exceptions, true, false, and equality), it lacks the capability to produce mock objects. That’s where OCMock comes in. OCMock is a library that works with Objective-c and provides methods that allow you to employ mock objects throughout your own applications. In this post, I’ll be walking through the setup of OCMock in Apple’s xCode environment and running through a few basic use cases for the OCMock library.

 Setting up OCMock

 Before we begin, here’s a rundown of my current environment:

  • xCode Version: 4.6.2
  • OCMock Version: 2.1
  • Objective-C Version: 2.0
Setting up OCMock is a subject that should be (and is) the subject of numerous blog posts. A good guide for setup purposes can be found on the OCMock website under the iOS tab at http://ocmock.org/ios/. There are a couple of gotcha’s in the setup that I ran into when I attempted it myself. First, xCode does not show all of the options for a project to you by default; you have to select the “show all files” button in the top left corner of the project screen to see them. The second is that, as Mulle Kybernetik puts it:

“The linker tries to be clever and only includes those symbols that it thinks are used. It gets this wrong with Objective-C categories, and we need to tell it that (a) we’re dealing with Objective-C and (b) that it should load all symbols for the OCMock library.”

If the path to your files is not clearly defined in at least 3(!) places, you will get errors and nothing will work correctly. A third gotcha; the default selection for the project settings is the main project. Your tests (and OCMock) are their own project by default. You have to switch the target from the main project to the test project otherwise you’ll have the right files referenced by the wrong project (and still get errors when you try to use OCMock to test).

OCMock comes in two flavors. There is a framework version and a static library version. In order to do iOS development, you’ll need to use the static library version NOT the framework. The difference between the static library and the framework is how they are deployed with the project. Static libraries are compiled and their binaries are integrated with the application binaries on a compile. Frameworks are linked to and then referenced. Since there’s not a great way to make sure that the end users will have a particular version of a framework installed (without installers/unnecessary complications), static libraries are the best choice for iOS development.

 Setting up Unit Tests

 

 LCM app running in simulator

 For this example, I coded up an app that will find the lowest number divisible by all integers from 1 to some arbitrary positive integer supplied by the user (restricted by the maximum value of an int). I originally solved this problem in Java with goals of efficiency and compactness. To facilitate this example, I ported it over to Objective-C following TDD practices. Here’ the original function and my thoughts on breaking it down into testable pieces:

    public String solve(int limit) {
        int[] numbers = new int[limit+1];
        BigInteger result = new BigInteger("1");
        for(int i = 0; i < numbers.length; i++) {
            numbers[i] = i;
        }
        for(int i = 2; i < numbers.length; i++) {
            if(i > 1) {
                result = result.multiply(new BigInteger(String.valueOf(numbers[i])));
                for(int j = i+1; j < numbers.length; j++) { 
                    if(numbers[j] > 1 && numbers[j] % numbers[i] == 0) { 
                        numbers[j] /= numbers[i];
                    }
                }
            }
        }
        return result.toString();

The Setup Phase

Here’s the first piece I considered:

public String solve(int limit) {
        int[] numbers = new int[limit+1];
        BigInteger result = new BigInteger("1");
        for(int i = 0; i < numbers.length; i++) {
            numbers[i] = i;
        }

These operations result in assignment and population (note the actual size of the array is limit+1). In the context of an iOS app, the limit would be a return from user input and would define the dimensions of the numbers array. The array population can be broken out of this section and into its own function to facilitate separate unit testing. In xCode, I wrote the following tests for this piece:

-(void)testInputFieldToNumberConversion {
    [[[inputField stub] andReturn:@"3"] text];
    [[[beController userLimit]expect] intValue];
    [beController convertStringFieldToInt:inputField];
    STAssertEquals(3, [[beController userLimit] intValue], @"User field to Number conversion error");
}

And

-(void)testArrayPopulation {
    NSNumber* testNumber = [[NSNumber alloc]initWithInt:3];
    NSMutableArray* testArray = [beController populateArrayToLimit:testNumber];
    STAssertTrue(4 == [testArray count], @"Array Population error");
}

In the two tests above I used two testing libraries. The STAssertTrue in the testArrayPopulation method is a member of Apple’s built-in OCUnit testing library. Above it, in the testInputFieldToNumberConversion method, I have made use of mock objects and stubs provided by OCMock to simulate an input field object and its text method. I also used the expect method provided by OCMock. The expect method will fail if the specified function is not called on its target object. It is a good way to make sure that program control is moving in the direction you designed it to. I have included the tested Objective-C methods below:

//
//  BEViewController.m
//  BlogExample
//
//  Created by Fairway on 5/3/13.
//  Copyright (c) 2013 Fairway. All rights reserved.
//

#import "BEViewController.h"

@interface BEViewController ()

@end

@implementation BEViewController

@synthesize userRange;
@synthesize userLimit;
@synthesize inputField;
@synthesize displayLabel;
@synthesize calculateButton;

-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if(self) {
        userLimit = [[NSNumber alloc]init];
        userRange = [[NSMutableArray alloc]init];
    }
    return self;
}

This is the setup of the Object variables ^

-(NSNumber*)convertStringFieldToInt:(UITextField *)textField {
    NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
    [f setNumberStyle:NSNumberFormatterDecimalStyle];
    userLimit = [f numberFromString: [textField text]];
    return userLimit;
}

Here is the conversion tested by testInputFieldToNumberConversion ^

-(NSMutableArray*)populateArrayToLimit:(NSNumber *)limit {
    int top = [limit intValue];
    for(int i = 0; i <= top; i++) {
        [userRange addObject:[NSNumber numberWithInt:i]];
    }
    return userRange;
}

And here is the population piece tested by testArrayPopulation ^

The next chunk is the meat of the algorithm and required a bit more detangling to make it testable.

 The Algorithm

for(int i = 2; i < numbers.length; i++) {
            if(i > 1) {
                result = result.multiply(new BigInteger(String.valueOf(numbers[i])));
                for(int j = i+1; j < numbers.length; j++) { 
                    if(numbers[j] > 1 && numbers[j] % numbers[i] == 0) { 
                        numbers[j] /= numbers[i];
                    }
                }
            }
        }
        return result.toString();

This algorithm executes by dividing all eligible higher factors by the current value of the array as referenced by the loop variable (by eligible I mean division produces a whole number). It takes the array produced by the assignment and population phase above it as input. Starting at index 2 (all indexes contain their corresponding integer 2 => 2 and so on), it will attempt to divide all subsequent items in the array by the current number. At each loop execution, the current number advances. If it’s (the new current number) greater than 1 (Meaning a new factor), the program will enter the loop, multiply the answer by the current number, and then attempt to divide the remaining array members by the current number once more. Here’s an example of what the array will look like over successive passes:

Algorithm Chart

(Note that ‘1’ values are not shown in the result array, but they do exist. Every composite number that does not introduce a new prime reduces to a one and is skipped when the algorithm reaches it)

As you can see from the diagram above, the original array is transformed from its initial form (populated with all the numbers 1…limit) into a state where it only contains prime factors common amongst all numbers in the original array. By multiplying each member, the resulting prime factor array will solve the problem by producing the least common multiple. That separation of operations between the production and aggregation of the prime array is a perfect place to separate out functionality for testing.

 Separation of Concerns

The aggregation and production phases of the algorithm provide a convenient breaking point from which separation of the current function is possible. I did so when I wrote the Objective-C code for this algorithm:

-(void)testArrayAggregation {
    NSMutableArray* testArray = [[NSMutableArray alloc]init];
    for(int i = 0; i < 4; i++) {
        [testArray addObject:[[NSNumber alloc]initWithInt:i]];
    }
    NSNumber* testNumber = [beController aggregateArray:testArray];
    STAssertEquals(6, [testNumber intValue], @"Array Aggregation error");
}

The above testArrayAggregation function tests to make sure that the aggregateArray function in the BEViewController is working correctly.

-(NSNumber*)aggregateArray:(NSMutableArray *)array {
    int ans = 1;
    for (int i = 2; i < [array count]; i++) {
        ans *= [array[i] intValue];
    }
    return [[NSNumber alloc]initWithInt:ans];
}

With the aggregation and setup pieces tested, the only remaining major component is the reduction piece that produces the array containing the answers. This piece took a bit more hardcoding to get correct, but tests the most common case the function will encounter:

-(void)testPrimeFactorGeneration {
    NSMutableArray* testArray = [[NSMutableArray alloc]init];
    for(int i = 0; i <= 10; i++) {
        [testArray addObject:[[NSNumber alloc]initWithInt:i]];
    }
    testArray[4] = [[NSNumber alloc]initWithInt:2];
    testArray[6] = [[NSNumber alloc]initWithInt:1];
    testArray[8] = [[NSNumber alloc]initWithInt:2];
    testArray[9] = [[NSNumber alloc]initWithInt:3];
    testArray[10] = [[NSNumber alloc]initWithInt:1];
    NSMutableArray* temp = [beController determinePrimeFactors: [beController populateArrayToLimit:[[NSNumber alloc]initWithInt:10]]];
    STAssertTrue([temp isEqualToArray:testArray], @"Prime factor generation method not working");
}

And its corresponding function:

-(NSMutableArray*)determinePrimeFactors:(NSMutableArray *)list {
    for(int i = 2; i < [list count]; i++) {
        if(i > 1 ) {
            for(int j = i+1; j < [list count]; j++) {
                if([list[j] intValue] > 1 && [list[j] intValue] % [list[i] intValue] == 0) {
                    int x = [list[j] intValue] / [list[i] intValue];
                    NSNumber *n = [[NSNumber alloc]initWithInt:x];
                    list[j] = n;
                }
            }
        }
    }
    return list;
}

This leaves only two pieces; showAnser and restoreBeginningState. The showAnswer function’s only purpose is to execute the other class functions in the correct order to produce the answer. In essence, it’s a type of miniature integration test; with all of the pieces tested individually, showAnswer tests their cooperation as a unit:

-(void)showAnswer:(id)sender {
    userLimit = [self convertStringFieldToInt: inputField];
    userRange = [self populateArrayToLimit: userLimit];
    NSNumber* number = [self aggregateArray: [self determinePrimeFactors: userRange]];
    if([number intValue] == 1) {
        number = 0;
    }
    [displayLabel setText:[number stringValue]];
    [self restoreBeginningState];
}

Its tests run through all program logic (with the exception of restoreBeginningState):

-(void)testThatAnswerThreeIsSix {
    [[[inputField stub] andReturn: @"3"] text];
    [[[beController displayLabel] expect] setText: @"6"];
    [beController showAnswer: inputField];
    [displayLabel verify];
    [inputField verify];
}

-(void)testThatAnswerTenIs2520 {
    [[[inputField stub] andReturn: @"10"] text];
    [[[beController displayLabel] expect] setText: @"2520"];
    [beController showAnswer: inputField];
    [displayLabel verify];
    [inputField verify];
}

-(void)testThatAnswerTwentyIs232792560 {
    [[[inputField stub] andReturn: @"20"] text];
    [[[beController displayLabel] expect] setText: @"232792560"];
    [beController showAnswer: inputField];
    [displayLabel verify];
    [inputField verify];
}

Last (but not least), I wrote a function that restores the Object variables to their original states. This allows the user to enter a new value after a calculation is performed and receive a valid answer (otherwise old data would produce bad answers):

-(void)restoreBeginningState {
    userRange = [[NSMutableArray alloc]init];
    userLimit = [[NSNumber alloc]init];
}

And its test:

-(void)testRestorationToOriginalState {
    [[beController userRange] addObject:[[NSNumber alloc]initWithInt:3]];
    [beController setUserLimit:[[NSNumber alloc]initWithInt:3]];
    STAssertTrue([[beController userRange]count] == 1, @"restoreBeginningState test error");
    STAssertTrue([[beController userLimit] integerValue] == 3,  @"restoreBeginningState test error");
    [beController restoreBeginningState];
    STAssertTrue([[beController userRange]count] == 0, @"userRange has not been cleared in restoreToOriginalState");
    STAssertTrue([beController userLimit] == nil,  @"userLimit has not been cleared in restoreBeginningState");
}

Executing the program in the simulator yields a working app that correctly produces the LCM of the range defined by user input. That correctness was definitely helped along by TDD and OCMock. Objective-C is not a language that I know well. In unfamiliar domains (where you’re more likely to introduce bugs) TDD becomes even more crucial than normal. In addition to its utility in helping you work through the use cases, interfaces, and method signatures in advance, TDD can also help you figure out how language constructs and libraries work in a new language. If you think you’re producing a primitive int and your test case bombs out with a pointer, you know the class of issue you’re dealing with before you attempt to integrate your functionality into your application. TDD practices are great for isolating and anticipating problems before they happen and can make programming in any language much more productive.

 Resources

If you’re interested on learning more about this topic, here are some related resources:

http://ocmock.org/ – Location of OCMock. Tutorials, install info, api

http://alexvollmer.com/posts/2010/06/28/making-fun-of-things-with-ocmock/ – Insightful blog about using OCMock

http://alexvollmer.com/posts/2010/06/01/cocoas-broken-tests/ – More from Vollmer focusing on unit testing

http://www.amazon.com/Test-Driven-iOS-Development-Developers-Library/dp/0321774183/ref=sr_1_1?ie=UTF8&qid=1368203991&sr=8-1&keywords=tdd+objective+c – A book specifically about TDD in Objective-C (And it’s not bad!)


Viewing all articles
Browse latest Browse all 53

Trending Articles