[iOS-Practice] 单元测试

关于单元测试

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块的最小单位来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 -- 维基百科

《你应该知道的单元测试》这篇文章对单元测试的基础思想做了很好的总结。
ObjC 中国的期刊在第15期也讨论了“测试”这个专题。

XCTest

XCTest是苹果公司提供的一个非常简单并且直接集成在 Xcode 中的测试框架。当工程创建时,Xcode 会自动为我们创建一个名为ProjectNameTests的路径并添加一个测试用例模板文件ProjectNameTests.m(如果创建时未添加,之后可以通过添加 target 的方式增加测试 bundle)。通过这个模板文件,我们可以了解XCTest框架的使用方法。

#import <XCTest/XCTest.h>

@interface UnitTestDemoTests : XCTestCase

@end

@implementation UnitTestDemoTests

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

@end

首先,我们的测试用例类要继承自XCTestCase类,其中[setUp]方法会在每个测试方法前执行,而[tearDown]方法会在每个测试方法后执行,真正的测试方法必须以 testXXX 的格式命名,且不能有参数。

测试时,快捷键command + u可以一次执行所有的测试,也可以点击每个测试方法旁的播放按钮执行单独的测试。

实践

那么,我们怎样来写一个测试用例呢?测试用例的意义在于,验证某个类的某个行为在某种上下文中是否能得到预期的结果。通常,我们可以根据 Given-When-Then 模式来组织我们的测试用例,将测试用例拆分成三个部分。

  • Given:准备测试功能的上下文,包括测试方法需要的参数等。
  • When:执行真正要测试的代码。
  • Then:根据功能执行的结果断言测试是否通过。

例:

- (void)testThatItDoesURLEncoding { 
    // given
    NSString *searchQuery = @"$content$amp;?@"; HTTPRequest *request = [HTTPRequest requestWithURL:@"/search?q=%@", searchQuery]; 
    // when
    NSString *encodedURL = request.URL;
    // then
    XCTAssertEqualObjects(encodedURL, @"/search?q=%24%26%3F%40");
}

在 Then 阶段,XCTest框架提供了多个断言宏供我们使用:

//生成一个失败的测试
XCTFail(format…)
//为空判断,a1为空时通过,反之不通过
XCTAssertNil(a1, format...) 
//不为空判断,a1不为空时通过,反之不通过
XCTAssertNotNil(a1, format…)
//当expression求值为TRUE时通过
XCTAssert(expression, format...)
//当expression求值为TRUE时通过
XCTAssertTrue(expression, format...) 
//当expression求值为False时通过
XCTAssertFalse(expression, format...) 
//判断相等,[a1 isEqual:a2]值为TRUE时通过
XCTAssertEqualObjects(a1, a2, format...) 
//判断不等,[a1 isEqual:a2]值为False时通过
XCTAssertNotEqualObjects(a1, a2, format...) 
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以)
XCTAssertEqual(a1, a2, format...) 
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用)
XCTAssertNotEqual(a1, a2, format...) 
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...) 
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 
 //异常测试,当expression发生异常时通过
XCTAssertThrows(expression, format...)
 //异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过
XCTAssertThrowsSpecific(expression, specificException, format...)
 //异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
XCTAssertNoThrow(expression, format…) //异常测试,当expression没有发生异常时通过测试;
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecific(expression, specificException, format...) 
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) 

另外,如果多个测试用例类需要一些相同的初始化条件,我们可以实现一个XCTestCase类的派生类作为基类。在这个类中实现一些公共的方法和属性。之后,测试用例类直接继承自这个基类,要注意在[setUp]方法和[tearDown]方法中调用 super 的实现。

网络请求的测试

由于单元测试是在主线程中进行的,因此如果只是在网络请求异步响应的方法中执行断言,那么测试在异步操作返回结果前就已经结束了,无法达到测试的目的。对于异步操作的测试,XCTest框架提供了这样一种机制,首先在测试方法中关联一个代表期望的XCTestExpectation实例,然后在测试方法结束前执行方法[- waitForExpectationsWithTimeout:handler:],该方法会执行一个 run loop 直到所有的期望实例执行了方法[- fulfill](即期望达成)或者达到超时时间。例如 AFNetworking 中的一个测试:

- (void)testDataTaskDoesReportDownloadProgress {
    NSURLSessionDataTask *task;

    __weak XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should equal 1.0"];
    task = [self.localManager
            dataTaskWithRequest:[self bigImageURLRequest]
            uploadProgress:nil
            downloadProgress:^(NSProgress * _Nonnull downloadProgress) {
                if (downloadProgress.fractionCompleted == 1.0) {
                    [expectation fulfill];
                }
            }
            completionHandler:nil];
    
    [task resume];
    [self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}

通过上述机制,虽然我们可以直接测试真正的网络请求,但是真实网络环境是非常复杂的,返回的响应具有不确定性,为了达到单元测试关注点单一的目的,我们可能需要模拟确定的网络请求响应数据。OHHTTPStubs 通过NSURLProtocol实现了模拟网络请求响应的功能,是在对网络请求相关代码进行单元测试时,非常好用的工具。这篇文章对其实现原理进行了介绍:《如何进行 HTTP Mock(iOS)》

如果是通过 Cocoapods 来安装管理 OHHTTPStubs 的话,那么默认在 test target 中 import 头是找不到 pods 中的类库的,需要在 test target 的 Build Settings 中设置 Header Search Paths,可以复制产品 target 中对应的值,而其中的 pods 路径别名也需要在 test target 中设置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 前言 如果有测试大佬发现内容不对,欢迎指正,我会及时修改。 大多数的iOS App(没有持续集成)迭代流程是这样的...
    默默_David阅读 1,650评论 0 4
  • 简介 测试目的:模拟多种可能性,减少错误,增强健壮性,提高稳定性。 测试种类:在iOS中的通常分为单元测试和UI测...
    i顺颂时宜阅读 9,097评论 0 39
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 单元测试不是一个小工程,需要多用些时间才能做好,不要希望通过这个文章就能掌握单元测试,这只是一个入门,需要自己动手...
    勇不言弃92阅读 7,788评论 9 60
  • 文章来自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鹏阅读 9,188评论 2 126