IOS Unit Testing With OCMock

关于OCMock的使用介绍,以备不时之需,转自:
http://fairwaytech.com/2013/05/ios-unit-testing-with-ocmock/


IOS Unit Testing With OCMock

Written by Gregory White on May 28 2013 - 2 Comments

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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicString solve(intlimit) {
    int[] numbers = newint[limit+1];
    BigInteger result = newBigInteger("1");
    for(inti = 0; i < numbers.length; i++) {
        numbers[i] = i;
    }
    for(inti = 2; i < numbers.length; i++) {
        if(i > 1) {
            result = result.multiply(newBigInteger(String.valueOf(numbers[i])));
            for(intj = i+1; j < numbers.length; j++) {
                if(numbers[j] > 1&& numbers[j] % numbers[i] == 0) {
                    numbers[j] /= numbers[i];
                }
            }
        }
    }
    returnresult.toString();

The Setup Phase

Here’s the first piece I considered:

?
1
2
3
4
5
6
publicString solve(intlimit) {
        int[] numbers = newint[limit+1];
        BigInteger result = newBigInteger("1");
        for(inti = 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:

?
1
2
3
4
5
6
-(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

?
1
2
3
4
5
-(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:

?
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
//
//  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];
    }
    returnself;
}

This is the setup of the Object variables ^

?
1
2
3
4
5
6
-(NSNumber*)convertStringFieldToInt:(UITextField *)textField {
    NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
    [f setNumberStyle:NSNumberFormatterDecimalStyle];
    userLimit = [f numberFromString: [textField text]];
    returnuserLimit;
}

Here is the conversion tested by testInputFieldToNumberConversion ^

?
1
2
3
4
5
6
7
-(NSMutableArray*)populateArrayToLimit:(NSNumber *)limit {
    inttop = [limit intValue];
    for(inti = 0; i <= top; i++) {
        [userRange addObject:[NSNumber numberWithInt:i]];
    }
    returnuserRange;
}

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

?
1
2
3
4
5
6
7
8
9
10
11
for(inti = 2; i < numbers.length; i++) {
            if(i > 1) {
                result = result.multiply(newBigInteger(String.valueOf(numbers[i])));
                for(intj = i+1; j < numbers.length; j++) {
                    if(numbers[j] > 1&& numbers[j] % numbers[i] == 0) {
                        numbers[j] /= numbers[i];
                    }
                }
            }
        }
        returnresult.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:

?
1
2
3
4
5
6
7
8
-(void)testArrayAggregation {
    NSMutableArray* testArray = [[NSMutableArray alloc]init];
    for(inti = 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.

?
1
2
3
4
5
6
7
-(NSNumber*)aggregateArray:(NSMutableArray *)array {
    intans = 1;
    for(inti = 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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
-(void)testPrimeFactorGeneration {
    NSMutableArray* testArray = [[NSMutableArray alloc]init];
    for(inti = 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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(NSMutableArray*)determinePrimeFactors:(NSMutableArray *)list {
    for(inti = 2; i < [list count]; i++) {
        if(i > 1 ) {
            for(intj = i+1; j < [list count]; j++) {
                if([list[j] intValue] > 1 && [list[j] intValue] % [list[i] intValue] == 0) {
                    intx = [list[j] intValue] / [list[i] intValue];
                    NSNumber *n = [[NSNumber alloc]initWithInt:x];
                    list[j] = n;
                }
            }
        }
    }
    returnlist;
}

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:

?
1
2
3
4
5
6
7
8
9
10
-(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):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(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):

?
1
2
3
4
-(void)restoreBeginningState {
    userRange = [[NSMutableArray alloc]init];
    userLimit = [[NSNumber alloc]init];
}

And its test:

?
1
2
3
4
5
6
7
8
9
-(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!)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 JavaScript 编写的记忆游戏(附源代码)   项目:JavaScript 记忆游戏(附源代码) 记忆检查游戏是一个使用 HTML5、CSS 和 JavaScript 开发的简单项目。这个游戏是关于测试你的短期 记忆技能。玩这个游戏 时,一系列图像会出现在一个盒子形状的区域中 。玩家必须找到两个相同的图像并单击它们以使它们消失。 如何运行游戏? 记忆游戏项目仅包含 HTML、CSS 和 JavaScript。谈到此游戏的功能,用户必须单击两个相同的图像才能使它们消失。 点击卡片或按下键盘键,通过 2 乘 2 旋转来重建鸟儿对,并发现隐藏在下面的图像! 如果翻开的牌面相同(一对),您就赢了,并且该对牌将从游戏中消失! 否则,卡片会自动翻面朝下,您需要重新尝试! 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox, 以获得更好、更优化的游戏体验。要玩游戏,首先,通过单击 memorygame-index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值