iOS单元测试

1.介绍

在讲XCTest之前我们先来了解一下单元测试。单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,通过开发者编写代码去验证被测代码是否正确的一种手段,例如编写一个测试函数去测试某一功能函数是否能正确执行达到预期效果。在实际项目开发中使用单元测试可以提高软件的质量,也可以尽量早的发现代码中存在的问题加以修正。

2. 简单使用

XCTest是Xcode自带的单元测试框架,我们可以使用该框架做功能性代码的白盒单元测试,以自测并增强代码健壮性。

2.1 项目中添加XCTest
2.1.1 创建项目时勾选该选项
  • 创建项目时勾选 Include Unit Tests选项

    image.png

  • 创建项目成功后,项目目录下即可看到对应的单元测试文件夹(先忽略SimpleProjectUITests UI测试)


    image.png
2.1.2 项目创建后添加
  • 点击Show the Test navigator选项可以看到现在我们项目中是未添加单元测试的:


    image.png
  • 点击下方➕按钮,选中New Unit Test Target选项,然后配置参数:
    截屏2020-08-06 下午5.14.26.png

    点击finish即可。
2.2 方法简单介绍

现在只有一个.m文件,里面有4个方法:

// 在每一个测试方法调用前,都会被调用
// 用来初始化 test 用例的一些初始值
- (void)setUp {
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

// 在每一个测试方法调用后,都会被调用
// 用来重置 test 方法的数值
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
}

- (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.
    }];
}

在编写测试代码时,需要知道以下几点:

  • setUp方法
    setUp方法会在XCTestCase的测试方法每次调用之前调用,所以可以把一些测试代码需要用的初始化代码和全局变量写在这个方法里;

  • tearDown
    在每个单元测试方法执行完毕后,XCTest会执行tearDown方法,所以可以把需要测试完成后销毁的内容写在这个里,以便保证下面的测试不受本次测试影响

  • 测试用例
    所有测试的方法都需要以test为前缀进行命名,比如- (void)testExample

  • 为业务类创建测试类
    对于每一个业务类,我们都会有一个对应的测试类,所有的测试类需要继承XCTestCase,比如:NetService对应NetServiceTest,如果类的内容太多,可以通过Category进行分类,如果某个方法暂时不想测试了,可以加一个Disable前缀。

2.3 简单使用
    1. 我们在项目里面创建一个Student类:
// Student.h 文件
@interface Student : NSObject

- (NSInteger)studyAddA:(NSInteger)a b:(NSInteger)b;

- (NSInteger)studyDeleteA:(NSInteger)a b:(NSInteger)b;

@end

// Student.m 文件
#import "Student.h"

@implementation Student

- (NSInteger)studyAddA:(NSInteger)a b:(NSInteger)b{
    NSInteger result = a + b;
    return result;
}

- (NSInteger)studyDeleteA:(NSInteger)a b:(NSInteger)b{
    NSInteger result = a - b;
    return result;
}

@end

    1. 然后创建Student对应的测试类:StudentTests:
#import "Student.h"

@interface StudentTests : XCTestCase

@property (nonatomic, strong) Student *student;

@end

@implementation StudentTests

- (void)setUp {
    self.student = [Student new];
}

- (void)tearDown {
    self.student = nil;
}

- (void)testStudentAdd {
    NSInteger result = [self.student studyAddA:2 b:3];
    XCTAssert(result == 5, @"结果计算出错");
}

@end
    1. 运行测试用例

代码编辑器边栏菱形按钮,测试单个用例
Test 导航栏,测试单个用例
快捷键⌘ + U测试全部用例
使用命令行工具 xcodebuild 可以测试单个用例,也可以测试全部用例。

image.png
    1. 观察测试结果
image.png
    1. 查看代码覆盖率
      打开Edit Scheme:
      image.png

勾选Gather coverage for:

image.png

然后重新,运行测试用例,观察结果:


image.png

3. 如何进行性能测试

性能测试通过度量代码块执行所消耗的时间长短,来衡量是否通过测试。
性能测试会运行想要评估的代码块十次,收集平均执行时间和运行的标准偏差。然后平均值与baseLine进行比较以评估成功或失败。

baseLine是我们指定的用来评估测试通过或者失败的值。我们也可以自己指定一个特定的值。

截屏2020-08-07 下午4.45.20.png

我们可以通过点击measureBlock:方法左边菱形圆心 icon ,来设置Baseline,设置之后需要点击save保存。之后再执行测试用例时,如果成功,左边的icon会从圆心变成一个 ✅。

3.1 如何进行性能测试

相关 API :

  • measureBlock:
- (void)testPerformanceOfMyFunction {

    [self measureBlock:^{
        // Do that thing you want to measure.
        MyFunction();
    }];
}
  • measureMetrics:automaticallyStartMeasuring:forBlock:
- (void)testMyFunction2_WallClockTime {
    [self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{

        // Do setup work that needs to be done for every iteration but you don't want to measure before the call to -startMeasuring
        SetupSomething();
        [self startMeasuring];

        // Do that thing you want to measure.
        MyFunction();
        [self stopMeasuring];

        // Do teardown work that needs to be done for every iteration but you don't want to measure after the call to -stopMeasuring
        TeardownSomething();
    }];
}

4. 异步测试

什么时候需要使用异步测试:

  1. 打开文档
  2. 在后台线程中执行的服务和网络活动
  3. 执行动画
  4. UI 测试时
4.1 异步测试XCTestExpectation

异步测试分为3个部分: 新建期望等待期望被履行履行期望

  • XCTestExpectation:测试期望,可以由测试类持有,也可以自己持有,自己持有测试期望时灵活性更好一些,你可以选择等待哪些期望。
// 测试类持有的初始化方法
XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];

// 自己持有的初始化方法
XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
  • waitForExpectations:timeout: :等待异步的期望代码执行,根据初始化方式不同,等待的方法不同。
// 测试类持有时的等待方法
[self waitForExpectationsWithTimeout:10.0 handler:nil];

// 自己持有时的等待方法
[self waitForExpectations:@[expect3] timeout:10.0];
  • fulfill :履行期望,并且适当加入XCTAssertTrue等断言,来验证测试结果。
XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];

[TTFakeNetworkingInstance requestWithService:apiRecordList completionHandler:^(NSDictionary *response) {
    XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
    [expect3 fulfill];
}];

[self waitForExpectations:@[expect3] timeout:10.0];
4.2 异步测试XCTWaiter

XCTWaiter是 2017 年新增的异步测试方案,可以通过代理方式来处理异常情况。

XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
    
XCTestExpectation *expect4 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
    
[TTFakeNetworkingInstance requestWithService:@"product.list" completionHandler:^(NSDictionary *response) {
    XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
    expect4 fulfill];
}];

XCTWaiterResult result = [waiter waitForExpectations:@[expect4] timeout:10 enforceOrder:NO];

XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);

XCTWaiterDelegate:如果委托是XCTestCase实例,下方代理被调用时会报告为测试失败。

// 如果有期望超时,则调用。 
- (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations;

// 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。
- (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation;

// 当某个期望被标记为被倒置,则调用。 
- (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation;

// 当 waiter 在 fullfill 和超时之前被打断,则调用。 
- (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter;

5. 断言记录

在写测试用例的时候,我们可以使用断言,下面是记录一下:

XCTFail(format…) 生成一个失败的测试; 
 
XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过; 
 
XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;
 
XCTAssert(expression, format...)当expression求值为TRUE时通过; 
 
XCTAssertTrue(expression, format...)当expression求值为TRUE时通过; 
 
XCTAssertFalse(expression, format...)当expression求值为False时通过; 
 
XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
 
XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过;
 
XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以); 
 
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
 
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试; 
 
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试; 
 
XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态) XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过; 
 
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过; 
 
XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
 
XCTAssertNoThrowSpecific(expression, specificException, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过; 
 
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
 
 
 
特别注意下XCTAssertEqualObjects和XCTAssertEqual。
 
XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。
 
XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。
 
对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES。例如

合理使用测试基类和测试工具类,可以避免大量重复测试代码。时间转换工具类是一个没有外部依赖的类,当一些对外部有依赖的类需要测试时,可以尝试 OCMock ,它能帮助你模拟数据。另外,当你觉得测试框架提供的断言方法无法满足你时,也可以试着使用 OCHamcrest

6. 未完待续

简单的记录一下,还有很多等待发现。。。

7. 参考

iOS开发之XCTest
官方文档翻译
在XCode中使用XCTest
iOS 单元测试和 UI 测试快速入门
官方文档
XCTest 测试实战
OCMock翻译

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