iOS单元测试及其应用

  • 0.绪论
    • TDD-测试驱动开发
    • BDD-行为驱动开发
  • 1.什么是单元测试
  • 2.为什么要做单元测试
  • 3.iOS单元测试⽅案
    • 测试框架:
    • 测试对象:
    • 测试工具:
    • 1.XCTest
      • 基本⽅法:
      • UT三步曲:
      • 注意事项:
      • 断⾔:
        • 真假断言:Test a condition that generates a true or false result
        • 等价断言:Check whether two values are equal or not equal.
        • 空/非空断言:Check whether a test condition is nil or non-nil.
        • 错误断言:Check whether a function call throws (or doesn’t throw) an error.
        • 无条件失败断言:Generate a failure immediately and unconditionally.
      • 异步操作测试:
      • 代码覆盖率
    • 2.OCMock
  • 4.示例Demo
    • 4.1 测试步骤
    • 4.2 案例实现
  • 5.思考
  • 参考文献

0.绪论

基础概念:BDD(Behavior Driven Development-行为驱动开发)与TDD(Test Driven Development-测试驱动开发)

TDD-测试驱动开发

Test-driven development 测试驱动开发,它是一种用一种测试先于编写代码的思想来指导软件开发。测试驱动开发是敏捷开发中的一项核心实践和技术,其原理是在开发功能代码之前,先编写单元测试用例代码,从而通过测试代码确定需要编写什么具体的开发代码。使用这种做法的结果是一套全面的单元测试,可以随时运行以提供软件可以正常工作的反馈。

BDD-行为驱动开发

Behavior-Driven Development,行为驱动开发是指以软件实际要达到的目标也就是需求设计来驱动整个开发过程,更关注于用户故事,即产品、开发者、测试人员一起头脑风暴,分析实际对应用户对于该软件的需求,然后将这些需求写成一个个的故事。BDD重点是软件开发过程中使用的语言和交互。开发者负责填充故事的内容满足用户需求,测试者负责检验这些故事的结果是否是用户所希望的成果。相比较TDD来说,这种开发形式可以尽可能的避免用户和开发者在沟通上的障碍,实现客户和开发者同时定义系统的需求,避免因为理解需求不充分而带来的不必要的工作量。

1.什么是单元测试

单元测试: Unit Testing,顾名思义就是对最小的一个单元代码或不受其他代码逻辑影响的函数等进行测试,对输入的参数和输出都要求比较明确。单元测试的目的是把程序里每个独立的最小单元隔离开来,保证其正确性,使得整个程序相对也更正确。(单元测试与接口测试的区别)

2.为什么要做单元测试

  • 单元测试作为敏捷开发实践的组成之⼀,⽬的是提⾼软件开发的效率,维持代码的健壮性,对我们的产品质量是非常重要的
  • 单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次有保证能够代码覆盖率达到100%的测试,是整个软件测试过程的基础和前提,单元测试防止了开发的后期因bug过多而失控,单元测试的性价比是最好的
  • 据统计,大约有80%的错误是在软件设计阶段引入的,并且修正一个软件错误所需的费用将随着软件生命期的进展而上升。错误发现的越晚,修复它的费用就越高,而且呈指数增长的趋势
  • 代码规范、优化,可测试性的代码
  • 放心重构
  • 自动化执行three-thousand times
  • 单元测试也有⼀些⾼级的作⽤,⽐如支持web⾃动化测试、APP自动化测试、API接口自动化测试等各种自动化测试

3.iOS单元测试⽅案

测试框架:

Github上的一些知名的开源库使用的测试框架:

常用的测试框架:

  • XCTest:Apple自带,与Xcode深度集成且享受Apple后续对XCTest升级维护。
  • KiWi:第三方插件,面向BDD思想

AFNetworking是一个轻量级的iOS网络通信类库。它建立在NSURLConnection和NSOperation等类库的基础上,让很多网络通信功能的实现变得十分简单,同时它支持HTTP请求和基于REST的网络服务(包括GET、POST、 PUT、DELETE等),且支持ARC。

采用⽅案:XCTest + OCMock (mock对象、桩程序)

测试对象:

⽹络请求接⼝
⼯具类⽅法
部分可测试视图逻辑
私有⽅法

测试工具:

1.XCTest

XCTest是Xcode集成的⼀套单元测试框架,以下是⼀些基本使⽤ :

基本⽅法:

- (void)setUp {
[super setUp];
// 每个test⽅法执⾏前调⽤,在这个测试⽤例⾥进⾏⼀些通⽤的初始化⼯作
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
[super tearDown];
// 每个test⽅法执⾏后调⽤
// 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.
}];
}

UT三步曲:

  1. 准备数据
  2. 调用方法
  3. 断言结果, 判断查看结果

示例代码:

//书写规范:方法名必须 以 “test” 开头
- (void)testStatus_fLevel {
    //1.数据准备
    BMKMapStatus *status = [[BMKMapStatus alloc] init];
    status.fLevel = 12.0;
 
 
    //2.调用方法
    [_mapView setMapStatus:status];
    BMKMapStatus *rstatus = [_mapView getMapStatus];
 
 
    //3.断言结果, 判断查看结果
    XCTAssertEqualWithAccuracy(rstatus.fLevel, 12.0, 0.1, "flevel wrong");
}
 
 
注:对于该用例,代码执行顺序:setUp--->testStatus_fLevel--->tearDown

注意事项:

  • 1.整个工程中应该有多个XCTest文件,每一个XCTests都是只有.m文件的,继承自XCTestCase(当然如果你有公共配置,其实可以先新建继承自XCTestCase类的一个Cocoa Touch Class类(如UserDefinedBaseTestCase)当作自己的XCTest所有类的父类,然后对于测试的基本配置/公用方法都可以在这个父类中实现;其他子类文件可以直接继承自UserDefinedBaseTestCase类就可以直接调用公有方法了)
  • 2.每个XCTests.m文件中,必然包含一个setUp方法和一个tearDown方法
  • 3.每个单元测试方法执行之前,XCTest会先执行setUp方法,所以我们可以在这个方法中进行初始化参数或测试环境的配置
  • 4.每个单元测试方法执行结束后,XCTest会执行tearDown方法,所以可以把需要测试完成后销毁的内容逻辑放在这里,以便保证下面的测试不受本次测试影响
  • 5.每个单元测试方法都应该以test开头

断⾔:

XCTAssert(expression, ...) XCTAssert(条件, 不满⾜条件的描述)

eg.
XCTAssert(a == 0, @“a不能等于0”)

真假断言:Test a condition that generates a true or false result

XCTAssertTrue(expression, ...) 当expression == false时报错

XCTAssertFalse(expression, ...) 当expression != false时报错

等价断言:Check whether two values are equal or not equal.

XCTAssertEqualObjects(expression1, expression2, format...) 当expression1不等于expression2时报错
XCTAssertNotEqualObjects(expression1, expression2, format...) 当expression1 等于expression2时报错

XCTAssertEqual(expression1, expression2, format...) 当expression1不等于 expression2时报错,这个断言⽤于C语⾔的标量
XCTAssertNotEqual(expression1, expression2, format...) 当expression1等于 expression2时报错,这个断言⽤于C语⾔的标量

XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...) 当 expression1和expression2之间的差别⾼于accuracy将报错。这种断言适⽤于 floats和doubles这些标量(C scalar type),两者之间的细微差异导致它们不完全相等,但是对所有的标量都有效
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...) 当expression1和expression2之间的差别低于accuracy将报错。这种断言适⽤于floats和doubles这些标量,两者之间的细微差异导致它们不完全相等,但是对所有的标量都有效

空/非空断言:Check whether a test condition is nil or non-nil.

XCTAssertNil(expression, format...) 当expression参数⾮nil时报错
XCTAssertNotNil(expression, format...) 当expression参数为nil时报错

错误断言:Check whether a function call throws (or doesn’t throw) an error.

XCTAssertThrows(expression, format...) 当expression不抛出异常时报错
XCTAssertThrowsSpecific(expression, exception_class, format...) 当expression 针对指定类不抛出异常时报错
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...) 当expression针对特定类和特定名字不抛出异常时报错。对于AppKit框 架或Foundation框架⾮常有⽤,抛出带有特定名字的NSException(NSInvalidArgumentException等等)
XCTAssertNoThrow(expression, format...) 当expression抛出异常时报错
XCTAssertNoThrowSpecific(expression, exception_class, format...) 当 expression针对指定类抛出异常时报错。任意其他异常都可以;也就是说它不会报 错
XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...) 当expression针对特定类和特定名字抛出异常时报错。
对于AppKit框架或Foundation框架⾮常有⽤,抛出带有特定名字的 NSException(NSInvalidArgumentException等

无条件失败断言:Generate a failure immediately and unconditionally.

XCTFail(...) 无条件返回失败

异步操作测试:

有些需要测试的方法可能会执行异步网络操作,也许并不能立即返回服务器响应的结果,这就需要我们考虑实现异步操作的单元测试,而XCTestCase内置支持测试异步方法

  • (XCTestExpectation *)expectationWithDescription:(NSString *)description:方法获取XCTestExpectation的实例,在这一模式下,测试完成的方法并不会被标记为测试用例通过;
  • (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler:方法用来等待操作完成,如果测试用例没有完成,那么将会调用回调处理的块
  • (void)fulfill:XCTestExpectation为fulfilled状态的话代表异步操作已经完成,可以检测是否符合预期

代码覆盖率

在自动化单元测试或功能测试中,经常使用代码覆盖率用来表示经过测试的代码的百分比,以保证我们的代码能够经过足够的测试。
我们可以开通xcode的代码覆盖收集,开通后就可以在导航栏的报告导航栏中看到测试记录及结果,还可以选择点击➡️跳转到测试代码处,还可以点击Coverage或者Logs标签查看覆盖代码和测试日志

image.png

2.OCMock

我们要测试的⽅法会引⽤很多外部依赖的对象,⽽我们没法控制这些外部依赖的对象。为了解决这个问题,我们需要⽤到Stub和Mock来模拟这些外部依赖的对象,从⽽控制它们。单独依靠XCTest难以完成Mock或者Stub但是结合OCMock可以在测试代码中实现这以下功能:
● Stub --- 桩程序, ⼈为地让⼀个对象对某个⽅法返回我们事先规定好的值
eg.
OCMStub([mockCar getCarBrand:[OCMArg any]]).andReturn(@"XXX");
[OCMArg any]表示可以为任意参数,若改为具体参数(如:张三),表示只有参数 为张三时才会触发stup,否则不会触发

● Mock --- 模拟对象, ⼀个对象, 它是对现有类的⾏为⼀种模拟(或是对现有接⼝实现的模拟), 它只能响应那些你添加了期望或者 stub 的⽅法
eg.
// 1.nice mock - 不会在⼀个没有被stub的⽅法被调⽤时抛出异常
Car *mockCar = OCMClassMock([Car class]);
// 2.vanilla mock - 在mock的⽣命周期中每⼀个⽅法调⽤都必须是stub过的⽅法。当调⽤⼀个没有stub的⽅法的时候会抛出⼀个异常
Car *mockCar = OCMStrictClassMock([Car class]);
// 3.partial mock - 当⼀个没有stub过的⽅法被调⽤了,这个⽅法会被转发到真 实的对象上。
Car *mockCar = OCMPartialMock([Car class]);

4.示例Demo

4.1 测试步骤

  1. 给定待测的接口、代码模块以及相应API文档
  2. 熟悉接口、理清内部逻辑、分支结构等细节
  3. 设计用例、编写单元测试
  4. 执行,断言,观察结果和预期

4.2 案例实现

  1. 对于给定接口(网络请求类接口),通过接口注释文档明确其接口功能,输入参数、输出结果、请求方式等信息:
    -接口功能:公交路线检索(仅支持市内)
    -输入参数:BMKTransitRoutePlanOption对象
    -返回结果:BOOL 值,YES/NO
    -请求类型:异步请求(即请求不在主线程操作)
/**
 *公交路线检索(仅支持市内)
 *异步函数,返回结果在BMKRouteSearchDelegate的onGetTransitRouteResult通知
 *@param transitRoutePlanOption 公交换乘信息类
 *@return 成功返回YES,否则返回NO
 */
- (BOOL)transitSearch:(BMKTransitRoutePlanOption *)transitRoutePlanOption;
  1. 通过代码走读(查看接口具体实现),熟悉接口、理清内部逻辑、分支结构等细节:
    -入参的合法性校验
    -代码分支结构是否合理
    -代码鲁棒性(健壮性、可靠性)
此处省略一万字
  1. 设计用例、编写单元测试
    -保证每个单元测试用例覆盖最小粒度的代码逻辑
    -全部单元测试用例叠加起来尽最大可能覆盖待测接口(模块)全部代码行、条件分支
#import 
#import 
@interface RoutePlanTransitSearchTest : XCTestCase
{
    BMKRouteSearch *routeSearch;
    XCTestExpectation *expectation;
    int tag;
}
@end
 
@implementation RoutePlanTransitSearchTest
 
- (void)setUp {
    [super setUp];
    routeSearch = [[BMKRouteSearch alloc]init];
    routeSearch.delegate = self;
     
}
 
- (void)tearDown {
    routeSearch.delegate = nil;
    routeSearch = nil;
    expectation = nil;
    [super tearDown];
}
 
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、option为null
 * 验证:返回NO
 */
- (void)testTransitRoutePlanSearch_Option_Null{
    Boolean flag = [routeSearch transitSearch:nil];
    XCTAssertFalse(flag, "option null return NO");
}
 
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、from节点为null
 * 2、to节点正常"
 * 3、city正常
 * 验证:返回NO
 */
- (void)testTransitRoutePlanSearch_From_Null{
    
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    end.cityName = @"北京";
    end.name = @"百度大厦";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = nil;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, "from null return NO");
}
 
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、from节点正常
 * 2、to节点null
 * 3、city正常
 * 验证:返回NO
 */
- (void)testTransitRoutePlanSearch_To_Null{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大厦";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = nil;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, "to null return NO");
}
 
/**
 * "定义一个BMKTransitRoutePlanOption对象
 * 1、from节点正常
 * 2、to节点正常
 * 3、city为null,即未指定在哪个城市检索
 * 验证:端上拦截,请求失败,返回NO
 */
- (void)testTransitRoutePlanSearch_City_Null{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大厦";
    end.cityName = @"北京";
    end.name = @"百度大厦";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = nil;
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];//预期:端上拦截,请求失败,返回NO
    XCTAssertFalse(flag, @"起终点使用关键字时,必须设定城市,不能为空或者空字符串");
    NSLog(@"testTransitRoutePlanSearch_City_Null flag %d", flag);
}
 
//city.length > MAX_CITY_LENGTH, 返回NO
- (void)testTransitRoutePlanSearch_Max_City{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大厦";
    end.cityName = @"北京";
    end.name = @"天通苑北一区";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, @"city lenght > max return false");
}
 
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、from的PlanNode节点,如北京,奎科科技大厦
 * 2、to的PlanNode节点,如北京,天通苑北一区"
 * 验证:
 * "返回的TransitRouteResult不为空,errorcode为no_error
 * 1、getRouteLines()不为空
 * 2、每条TransitRouteLine中getWayPoints()不为空,getAllStep()不为空
 * 3、每个路段节点,getEntrance()、getStepType()、getExit()、getInstructions()、getVehicleInfo()、不为空
 * 4、当路段公交或地铁,VehicleInfo中getPassStationNum()、getTitle()、getTotalPrice()、getUid()、getZonePrice()不为空"
 */
- (void)testTransitRoutePlanSearch_Normal{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大厦";
    end.cityName = @"北京";
    end.name = @"天通苑北一区";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
 
- (void)testTransitRoutePlanSearch_Normal_Pt{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.pt = CLLocationCoordinate2DMake(40.0477880000,116.3132600000);
    end.pt = CLLocationCoordinate2DMake(40.0830240000,116.4239230000);
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、from的PlanNode节点,通过withCityCodeAndPlaceName设置,如131,奎科科技大厦
 * 2、to的PlanNode节点,通过withCityNameAndPlaceName设置,如北京,天通苑北一区
 * 3、policy为EBUS_NO_SUBWAY"
 *
 * "返回的TransitRouteResult不为空,errorcode为no_error
 * 1、getRouteLines()不为空
 * 2、每条TransitRouteLine中getWayPoints()不为空,getAllStep()不为空
 * 3、每个路段节点,getEntrance()、getStepType()不为SUBWAY,getExit()、getInstructions()、getVehicleInfo()、不为空
 * 4、当路段公交或地铁,VehicleInfo中getPassStationNum()、getTitle()、getTotalPrice()、getUid()、getZonePrice()不为空"
 */
- (void)testTransitRoutePlanSearch_Normal_With_Policy{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大厦";
    end.cityName = @"北京";
    end.name = @"天通苑北一区";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
 
/**
 * "定义一个TransitRoutePlanOption对象
 * 1、from的PlanNode节点,通过withCityCodeAndPlaceName设置,如131,”窝窝窝“
 * 2、to的PlanNode节点,通过withCityNameAndPlaceName设置,如北京,”哈哈哈哈“
 * 返回的TransitRouteResult不为空,errorcode为result_not_found
 */
- (void)testTransitRoutePlanSearch_Result_Not_Found{
    tag = 2;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"gute street";
    end.cityName = @"北京";
    end.name = @"gute platz";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return true");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
    }
 
- (void)testTransitRoutePlanSearch_Result_Ambiguous{
    tag = 3;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"百度大厦";
    end.cityName = @"北京";
    end.name = @"小明";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return true");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
     
}
 
- (void)onGetTransitRouteResult:(BMKRouteSearch*)searcher result:(BMKTransitRouteResult*)result errorCode:(BMKSearchErrorCode)error{
    switch (tag) {
        case 1:
            XCTAssertEqual(error, BMK_SEARCH_NO_ERROR, @"error code wrong");
            XCTAssertNotNil(result, @"result nil");
            XCTAssertNotNil(result.routes, @"routes nil");
       
            for (BMKTransitRouteLine* plan in result.routes) {
                XCTAssertTrue(plan.distance > 0, @"plan distance 0");
                XCTAssertNotNil(plan.duration, @"plan duration nil");
                XCTAssertNotNil(plan.starting, @"plan start nil");
                XCTAssertNotNil(plan.terminal, @"plan end nil");
                XCTAssertNotNil(plan.steps, @"plan steps nil");
            }
            break;
        case 2:
            XCTAssertEqual(error, BMK_SEARCH_RESULT_NOT_FOUND, @"error code wrong");
            XCTAssertNil(result, @"result nil");
            break;
        case 3:
            XCTAssertEqual(error, BMK_SEARCH_AMBIGUOUS_ROURE_ADDR, @"error code wrong");
            XCTAssertNotNil(result, @"result nil");
            XCTAssertNotNil(result.suggestAddrResult, @"suggestAddrResult nil");
            XCTAssertNil(result.routes, @"routes nil");
            break;
        default:
            break;
    }
    [expectation fulfill];
}
 
@end
  1. 执行,断言,观察结果和预期


    image.png

5.思考

  1. 单元测试的覆盖率越全越好,当覆盖率(分支覆盖率、行覆盖率)达到百分百时就能说明待测接口或模块没有问题了?
    是否考虑代码分支逻辑的正确性

  2. 单元测试,除了关注待测对象的功能正确性之外,是否还需要关注其他方面?
    用户体验:错误码类型、日志提示的合理性|可读性

  3. 对于网络请求类接口,如何保证case运行的稳定性?
    考虑网络延迟,延迟操作,等待返回结果,具体方法:
    (1)在主线程中适当sleep,等待返回结果
    (2)在单测内部延迟判断

_expectation = [self expectationWithDescription:@"search result"];
 
BMKReverseGeoCodeSearchOption *rgcOption = [[BMKReverseGeoCodeSearchOption alloc] init];
rgcOption.location = CLLocationCoordinate2DMake(40.049850, 116.279920);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    BOOL result = [_geoSearch reverseGeoCode:rgcOption];
    XCTAssertTrue(result, @"geoSearch should return true");
});
 
[self waitForExpectationsWithTimeout:5.0 handler:nil];

参考文献

  1. https://developer.apple.com/documentation/xctest
  2. https://ocmock.org/
  3. https://github.com/kiwi-bdd/Kiwi
  4. https://www.zhihu.com/question/28729261

你可能感兴趣的:(iOS单元测试及其应用)