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!)

极简JAVA学习营第五期

01-19
想学好JAVA必须要报两万的培训班吗? 【课程背景】 JAVA是市场份额最大的编程语言,每天各大招聘网站上都会有数万个JAVA开发工程师的在招岗位,但是JAVA的技术体系庞大复杂,要想扎实掌握JAVA不是一件容易的事,线上学习相比线下两万起的高昂费用便宜了很多,而且具备学习时间灵活的优势,但是线上学习的劣势也很明显,没有线下那种学习氛围,碰到问题没法解决,在家学习很容易偷懒,极简JAVA学习营充分考虑到这些问题,通过每日实战编程练习,分队pk,助教答疑,作业点评,作业讲解,项目答辩等诸多环节充分激发你的学习热情,解决你学习中碰到的问题,让你花十分之一的钱学到JAVA的精髓,开启你的人生逆袭之路。 【专项的贴心服务】 1. 学练结合:定期布置视频任务和编程实战练习:通过每天的视频任务统一大家的进度,以便同学更好的交流,针对每天的任务会有相应的编程实战练习,通过练习内化知识。 2. 分队PK:将就业营的同学分成几队,通过作业统计表统计每队提交作业情况进行PK,激发你的学习动力。 3. 助教讲师答疑:碰到任何问题,发到群里,助教和讲师十分钟内帮你解决问题,扫清学习中的障碍。 4. 助教点评讲解作业:你每天提交作业都会有助教进行点评,让你知道有什么问题怎么解决,每三天一次视频讲解作业,互动解答问题 5. 项目答辩:每个阶段学完会有项目答辩,通过做项目巩固前一阶段的知识点,锻炼编码能力。 【往期训练营学习展示】 【套餐内容简介】 本套课以市场就业和职位需求为核心,从JAVA入门到多领域实战,设计出学习路线,共分为二十大模块,分别是:JAVA面向对象、Object类与常用API、集合框架、IO流、反射注解、多线程与网络编程、Object类与常用API等等。 同时采用理论讲解加实战演练的方式,既能让学员听懂听明白达到理解透彻,又能够在一个个真实实战案例中,让学员掌握真正有用的开发技能,从而进阶 JAVA 工程师! 套餐中一共包含21门JAVA程,助你从零进阶JAVA工程师! 阶段一:JAVA基础 课程1:《极简JAVA学习营开营篇》 课程2:《极简JAVA:JAVA面向对象》 课程3:《极简JAVA:Object类与常用API》 课程4:《极简JAVA:集合框架》 课程5:《极简JAVA:IO流》 课程6:《极简JAVA:反射注解》 课程7:《极简JAVA:多线程与网络编程》 阶段二:数据库入门 课程8:《极简JAVA:MySql数据库》 课程9:《极简JAVA:JDBC与连接池》 阶段三:JAVA WEB 课程10:《极简JAVA:HTML5与CSS3》 课程11:《极简JAVA:极简JAVA十一:Javascript与Jquery》 课程12:《极简JAVA:BootStrap》 课程13:《极简JAVA:JAVA Web》 阶段四:框架实战 课程14:《极简JAVA:Mavean入门》 课程15:《极简JAVA:MyBatis框架》 课程16:《极简JAVA:Spring框架》 课程17:《极简JAVA:Spring Mvc》 课程18:《极简JAVA:Oracle数据库》 课程19:《极简JAVA:Git入门》 课程20:《极简JAVA:Linux入门》 课程21:《极简JAVA:SpringBoot》 【课程特色】 1、易理解:讲师思路清晰、节奏明确、从易到难讲解透彻明白; 2、知识全:知识全面系统,从JAVA入门到实战,由易到难,让你彻底掌握JAVA开发; 3、重实战:涵盖大量实战项目,锻炼你的动手实操能力,面向工作编程; 【面向人群】 1、在校计算机专业或者对软件编程感兴趣的学生; 2、零基础想学JAVA却不知道从何入手 3、囊中羞涩,面对两万起的JAVA培训班不忍直视 4、在职没有每天大块的时间专门学习JAVA 这么细致的服务,这么好的氛围,这样的学习效果,你还等什么?赶紧报名吧,抓紧抢位,本期只招100人,错过只有等时间待定的下一期了
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值