iOS 开发-单元测试

前言

维基百科对单元测试的定义如下:

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。

在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

根据不同场景,单元的定义也不一样,通常我们将C语言的单个函数或者面向对象语言的单个类视作测试的单元。在使用单元测试的过程中,我们要知道这一点:

单元测试并不是为了证明代码的正确性,它只是一种用来帮助我们发现错误的手段

单元测试不是万能药,它确实能帮助我们找到大部分代码逻辑上的bug,同时,为了提高测试覆盖率,这能逼迫我们对代码不断进行重构,提高代码质量等。

内置单元测试框架

在Xcode4.x中集成了测试框架OCUnit,根据测试的目的大致可以将单元测试分为这三类:

  • 性能测试:测试代码执行花费的时间
  • 逻辑测试:测试代码执行结果是否符合预期
  • 异步测试:测试多线程操作代码

在我们新建项目的时候,已经默认选择创建单元测试的框架,除了Unit Tests之外还有一个UI Tests是iOS9推出的新特性,针对UI界面的单元测试框架。在创建项目之后,会自动生成一个appName+Tests的文件夹目录,下面存放着单元测试的文件

一个标准的测试类文件代码如下。其中setUp会在每一个测试用例开始前调用,用来初始化相关数据;tearDown在测试用例完成后调用,可以用来释放变量等结尾操作;testPerformanceExample中的会将方法中的block代码耗费时长打印出来;最后的testExample用来执行我们需要的测试操作,正常情况下,我们不使用这个方法,而是创建名为test+测试目的的方法来完成我们需要的操作:

测试用例

在每个测试用例方法的左侧有个菱形的标记,点击这个标记可以单独的运行这个测试方法。如果测试通过没有发生任何断言错误,那么这个菱形就会变成绿色勾选状态。使用快捷键command+U直接依次调用所有的单元测试。另外,可以在左侧的文件栏中选中单元测试栏目,然后直观的看到所有测试的结果。同样的点击右侧菱形位置的按钮可以运行单个测试方法或者文件:

单元测试总览

另外,为了保证单元测试的正确性,我们应当保证测试用例中只存在一个类或者只发生一个类变量的属性修改。下面是我们测试中常用的宏定义

XCTAssertNotNil(a1, format…) 当a1不为nil时成立

XCTAssert(expression, format...) 当expression结果为YES成立

XCTAssertTrue(expression, format...) 当expression结果为YES成立;

XCTAssertEqualObjects(a1, a2, format...) 判断相等,当[a1 isEqualTo: a2]返回YES的时候成立

XCTAssertEqual(a1, a2, format...) 当a1==a2返回YES时成立

XCTAssertNotEqual(a1, a2, format...) 当a1!=a2返回YES时成立

逻辑测试

笔者新建了一个用以测试的model类,该类提供了三个接口。需要注意的是,在逻辑测试的某个操作步骤前后,应该有对应的数据发生了改变,这样才能够方便我们进行测试:

@interface LXDTestsModel : NSObject

@property (nonatomic, readonly, copy) NSString * name;

@property (nonatomic, readonly, strong) NSNumber * age;

@property (nonatomic, readonly, assign) NSUInteger flags;

+ (instancetype)modelWithName: (NSString *)name age: (NSNumber *)age flags: (NSUInteger)flags;

- (instancetype)initWithDictionary: (NSDictionary *)dict;

- (NSDictionary *)modelToDictionary;

@end

在测试用例中,我定义了一个testModelConvert方法用来测试模型跟json之间的转换是否正确:

- (void)testModelConvert

{

NSString * json = @"{\"name\":\"SindriLin\",\"age\":22,\"flags\":987654321}";

NSMutableDictionary * dict = [[NSJSONSerialization JSONObjectWithData: [json dataUsingEncoding: NSUTF8StringEncoding] options: kNilOptions error: nil] mutableCopy];

LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict];

XCTAssertNotNil(model);

XCTAssertTrue([model.name isEqualToString: @"SindriLin"]);

XCTAssertTrue([model.age isEqual: @(22)]);

XCTAssertEqual(model.flags, 987654321);

XCTAssertTrue([model isKindOfClass: [LXDTestsModel class]]);

model = [LXDTestsModel modelWithName: @"Tessie" age: dict[@"age"] flags: 562525];

XCTAssertNotNil(model);

XCTAssertTrue([model.name isEqualToString: @"Tessie"]);

XCTAssertTrue([model.age isEqual: dict[@"age"]]);

XCTAssertEqual(model.flags, 562525);

NSDictionary * modelJSON = [model modelToDictionary];

XCTAssertTrue([modelJSON isEqual: dict] == NO);

dict[@"name"] = @"Tessie";

dict[@"flags"] = @(562525);

XCTAssertTrue([modelJSON isEqual: dict]);

}

逻辑测试的目的是为了检测在代码执行前后发生的变化是否符合预期,因此可以说80%左右的单元测试都是逻辑测试。最开始笔者学习单元测试的时候总有一种无从下手的感觉,但是当你从无形抽象的逻辑操作找到了数据变化的规律的时候,对应的单元测试就能很快的写出来了

性能测试

相较于上面的逻辑测试,性能测试的地位有些尴尬。在现今的开发环境下,我们已经能通过 instrument工具很好的查找到项目中的代码耗时点,性能测试就有种弃之可惜,食之无味的感觉了。但是为了本文的完整性,还是将这个补充完毕。笔者在测试model类中添加了类方法,用来随机生成100个类实例对象,并且在每次创建对象后让线程休眠一段时间来模拟耗时操作:

+ (NSArray<LXDTestsModel *> *)randomModels

{

NSMutableArray * models = @[].mutableCopy;

NSArray * names = @[

@"SindriLin", @"Bison", @"XiongZengHui", @"ZengChengChun", @"Tessie"

];

NSArray * ages = @[

@15, @20, @25, @30, @35

];

NSArray * flags = @[

@123, @456, @789, @012, <a href="http://www.jobbole.com/members/234">@234</a>

];

for (NSUInteger idx = 0; idx < 100; idx++) {

LXDTestsModel * model = [LXDTestsModel modelWithName: names[arc4random() % names.count] age: ages[arc4random() % ages.count] flags: [flags[arc4random() % flags.count] unsignedIntegerValue]];

[models addObject: model];

[NSThread sleepForTimeInterval: 0.01];

}

return models;

}

运行测试用法后控制台会输出下面的信息,其中红框中表示执行代码总耗时,在此demo中总共运行了11.015秒的时长

性能测试输出

虽然性能测试的定位确实有些鸡肋,但是另一方面,直接使用单元测试来获取某段代码的执行时间要比使用instrument快的多。通过性能测试直观的获取执行时间后,我们可以根据需要来决定是否将这些代码放到子线程中执行来优化代码(很多时候,数据转换会占用大量的CPU计算资源)

异步测试

由于单元测试是在主线程中进行的,因此异步操作的测试在执行完毕之前,往往已经结束了。为了实现异步测试,笔者采用while()的方式无限循环等待,为了实现这个效果,我在LXDTestsModel头文件中添加了一个NSData类型的属性以及一个异步操作的接口方法,通过判断这个属性值来实现效果:

- (void)asyncConvertToData

{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSDictionary * modelJSON = nil;

for (NSInteger idx = 0; idx < 20; idx++) {

modelJSON = [self modelToDictionary];

[self setValuesWithDictionary: modelJSON];

[NSThread sleepForTimeInterval: 0.001];

}

_data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil];

});

}

上面的代码在系统创建的默认等级的子线程中执行了一段耗时代码,最后把json转换成NSData数据保存在自身的属性中。对应的异步测试代码如下:

- (void)testAsync

{

NSDictionary * dict = @{

@"name": @"SindriLin",

@"age": @22,

@"flags": @987654321

};

LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict];

XCTAssertNotNil(model);

[model asyncConvertToData];

while (model.data == nil) {

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);

NSLog(@"waiting");

}

XCTAssertNotNil(model.data);

NSLog(@"convert finish %@", model.data);

}

同样的,如果你的异步操作是网络请求,那么在执行的回调外对获取的数据类型加上__block修饰,然后判断这个获取的数据是否不为空来停止循环。另外最重要的是你必须在你的死循环中加入CFRunLoopRunInModel这个函数的调用来保证即便是在等待的情况下,你的主线程仍然能处理其他的事情。

__block BOOL complete = NO;

__block NSData * data = nil;

[network POST: @"http://xxxxxxx" parameters: nil completion: ^(NSData * receiveData) {

data = receiveData;

complete = YES:

}];

while (!complete) {

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);

NSLog(@"requesting");

}

尾言

最开始笔者一度认为单元测试是个比较考验技术的东西,但恰恰相反的,单元测试的使用与概念是相当简单的一个东西,难点在于不知道怎么用,这就需要我们持续的使用练习才能更好的服务于我们的开发。此外,常用的第三方框架例如YYModel、AFNetworking、Alamofire等等优秀框架中也有对框架自身编写的单元测试,学习仿写这些单元测试也是快速提升自己的一种手段。

很多时候,我们的项目中难免发生多个类之间的交互处理,而这种操作非常的不好调试。单元测试的原则之一就在于我们用来测试的代码要求功能很单一,这其实与良好的代码设计的思想是非常相符的。一方面来说,良好的代码结构设计可以让我们的测试用例的构建更加快速简单;反过来单元测试逼着我们去想办法减少类之间的耦合以此来减少甚至排除测试的干扰。无论如何,如果你想成为更好的开发者,单元测试是我们快速提升代码认知的重要手段之一。

时间: 2024-10-22 15:50:34

iOS 开发-单元测试的相关文章

iOS开发中的单元测试(三)——URLManager中的测试用例解析

本文转载至 http://www.cocoachina.com/cms/plus/view.php?aid=8088 此前,我们在<iOS开发中的单元测试(一)&(二)>中介绍了从使用者的角度对比当下比较流行的两款单元测试框架OCUnit和GHUnit,这篇文章中我们将介绍一款导航控件URLManager. URLManager是一个基于UINavigationController和UIViewController,以URL Scheme为设计基础的导航控件,目的是实现ViewCont

iOS开发:XCTest单元测试(附上一个单例的测试代码)

测试驱动开发并不是一个很新鲜的概念了.在我最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确.我所学习第一门语言是c语言,用的最多的是在算法设计上,那时候最常做的事情就是编写了一段代码,如何编译运行,查看结果是否正确,很多时候,还得自己想很多特殊的(比如说零值,边界值)测试数据来检测所写代码.算法是否正确.那个时候,感觉还好,比较输出只是只是控制台的一个简单的数字或者字符.在学习iOS开发中,很多时候也是要测试的,这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来

iOS开发之单元测试

开始之前 本文侧重讲述如何在iOS程序的开发过程中使用单元测试.使用Xcode自带的OCUnit作为测试框架. 一.单元测试概述 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性.其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰).它是对软件质量的一种保证,例如重构之后我们需要保证软件产品的正常运行. 很多人认为编写单元测试没有用是认为单元

Android &amp;Swift iOS开发:语言与框架对比

转载自:http://www.infoq.com/cn/articles/from-android-to-swift-ios?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=articles_link&utm_content=link_text 从Android到Swift iOS开发:语言与框架对比 我从2009年开始做Android开发,开始接触Swift是在2014年底,当时组里曾经做过一个Demo App,感觉技术还

iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角)

iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角) 2015-04-05 15:25 2482人阅读 评论(1) 收藏 举报开源框架 图像: 1.图片浏览控件MWPhotoBrowser       实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等操作.      下载:https://github.com/mwaterfall/MWPhotoBrowser目前比较活跃的社区仍旧是Github,

【IOS】Mac和IOS开发资源汇总

本文主要汇集一些苹果开发的资源,会经常更新,建议大家把这篇文章单独收藏(在浏览器中按**command+D**). 今天收录了许多中文网站和博客.大家一定要去感受一下哦. 如果大家有知道不错的站点,可以告诉我.  目录 1.苹果官方文档 2.邮件列表 3.论坛 4.网站 5.博客 6.大会 7.播客和录像 正文 1.苹果官方文档 构建iOS程序:下面的这篇文章介绍了 iOS 程序开发的过程: Start Developing iOS Apps Today 构建Mac OS X程序:下面这篇文章介

0516.32款iOS开发插件和工具介绍[效率]

插件和工具介绍内容均收集于网络,太多了就不一一注明了,在此谢过! 1.Charles 为了调试与服务器端的网络通讯协议,常常需要截取网络封包来分析.Charles通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析.一个可查看所有HTTP和SSL/HTTPS流量的工具.这款工具对于你测试和服务器端进行交互的应用非常有用 2.xScope xScope带有六种不同的工具,帮助每一个设计者快速.精确的完成工作,这些工具功能灵活.强大,包括∶量

iOS开发 非常全的三方库、插件、大牛博客等等

UI 下拉刷新 EGOTableViewPullRefresh- 最早的下拉刷新控件. SVPullToRefresh- 下拉刷新控件. MJRefresh- 仅需一行代码就可以为UITableView或者CollectionView加上下拉刷新或者上拉刷新功能.可以自定义上下拉刷新的文字说明.具体使用看"使用方法". (国人写) XHRefreshControl- XHRefreshControl 是一款高扩展性.低耦合度的下拉刷新.上提加载更多的组件.(国人写) CBStoreHo

ios 开发中 动态库 与静态库的区别

使用静态库的好处 1,模块化,分工合作 2,避免少量改动经常导致大量的重复编译连接 3,也可以重用,注意不是共享使用 动态库使用有如下好处: 1使用动态库,可以将最终可执行文件体积缩小 2使用动态库,多个应用程序共享内存中得同一份库文件,节省资源 3使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的. 从1可以得出,将整个应用程序分模块,团队合作,进行分工,影响比较小. 等其他好处, 从2可以看出,其实动态库应该叫共享库,那么从这个意义上来说,苹果禁止iOS开