相信在国内一些中小型公司,开发者很少会去写软件测试相关的代码。当然这背后有一些原因在。本文就讲讲iOS开发中的软件测试相关的内容。
一、测试的重要性
测试很重要!测试很重要!测试很重要!重要的事情说三遍。
场景1:每次我们写完代码后都需要编译运行,以查看应用程序的表现是否符合预期。假如改动点、代码量小,那验证成本低一些,假如不符合预期,则说明我们的代码有问,人工去排查问题花费的时间也少一些。假如改动点很多、受影响的地方较多,我们首先要大概猜测受影响的功能,然后去定位问题、排查问题的成本就很高。
场景2:你新接手的SDK某个子功能需要做一次技术重构。但是你只有在公司内部的代码托管平台上可以看到一些Readme、接入文档、系统设计文档、技术方案评估文档等一堆文档。可能你会看完再去动手重构。当你重构完了,找了公司某条业务线的App接入测试,点了几下发现发生了奔溃。
心想,本地测试、debug都正常可是为什么接入后就Crash了。其实想想也好理解,你本地重构只是确保了你开发的那个功能运行正常,你很难确保你写的代码没有影响其他类、其他功能。假如之前的SDK针对每个类都有单元测试代码,那你在新功能开发完毕后完整跑一次单元测试代码就好了,保证每个UnitTest都通过、分支覆盖率达到约定的线,那么基本上是没问题的。
场景3:在版本迭代的时候,计划功能A,从开发、联调、测试、上线共2周时间。老司机做事很自信,这么简单的UI、动画、交互,代码风骚,参考服务端的「领域驱动」在该feature开发阶段落地试验了下。联调、本地测试都通过了,还剩3天时间,本以为测试1天,bugfix一天,最后一天提交审核。代码跟你开了个玩笑,测试完n个bug(大大超出预期)。为了不影响App的发布上架,不得不熬夜修bug。将所有的测试都通过测试工程师去处理,这个阶段理论上质量应该很稳定,不然该阶段发现代码异常、技术设计有漏洞就来不及了,你需要协调各个团队的资源(可能接口要改动、产品侧要改动),这个阶段造成改动的成本非常大。
相信大多数开发者都遇到过上述场景的问题。其实上述这几个问题都有解,那就是“软件测试”。
二、软件测试
1.分类
软件测试就是在规定的条件下对应用程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。
合理应用软件测试技术,就可以规避掉第一部分的3个场景下的问题。
软件测试强调开发、测试同步进行,甚至是测试先行,从需求评审阶段就先考虑好软件测试方案,随后才进行技术方案评审、开发编码、单元测试、集成测试、系统测试、回归测试、验收测试等。
软件测试从测试范围分为:单元测试、集成测试、系统测试、回归测试、验收测试(有些公司会谈到“冒烟测试“,这个词的精确定义不知道,但是学软件测试课的时候按照范围就只有上述几个分类)。工程师自己负责的是单元测试。测试工程师、QA负责的是集成测试、系统测试。
单元测试(UnitTesting):又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。「单元」的概念会比较抽象,它不仅仅是我们所编写的某个方法、函数,也可能是某个类、对象等。
软件测试从开发模式分为:面向测试驱动开发 TDD(Test-drivendevelopment)、面向行为驱动开发BDD(Behavior-drivendevelopment)。
2.TDD
TDD的思想是:先编写测试用例,再快速开发代码,然后在测试用例的保证下,可以方便安全地进行代码重构,提升应用程序的质量。一言以蔽之就是通过测试来推动开发的进行。正是由于这个特点,TDD被广泛使用于敏捷开发。
也就是说 TDD模式下,首先考虑如何针对功能进行测试,然后去编写代码实现,再不断迭代,在测试用例的保证下,不断进行代码优化。
优点:目标明确、架构分层清晰。可保证开发代码不会偏离需求。每个阶段持续测试。
缺点:技术方案需要先评审结束、架构需要提前搭建好。假如需求变动,则前面步骤需要重新执行,灵活性较差。
3.BDD
BDD即行为驱动开发,是敏捷开发技术之一,通过自然语言定义系统行为,以功能使用者的角度,编写需求场景,且这些行为描述可以直接形成需求文档,同时也是测试标准。
BDD的思想是跳出单一的函数,针对的是行为而展开的测试。BDD关心的是业务领域、行为方式,而不是具体的函数、方法,通过对行为的描述来验证功能的可用性。BDD使用DSL(DominSpecificLanguage)领域特定语言来描述测试用例,这样编写的测试用例非常易读,看起来跟文档一样易读,BDD的代码结构是Given->When->Then。
优点:各团队的成员可以集中在一起,设计基于行为的计测试用例。
4.对比
根据特点也就是找到了各自的使用场景,TDD主要针对开发中的最小单元进行测试,适合单元测试。而BDD针对的是行为,所以测试范围可以再大一些,在集成测试、系统测试中都可以使用。
TDD编写的测试用例一般针对的是开发中的最小单元(比如某个类、函数、方法)而展开,适合单元测试。
BDD编写的测试用例针对的是行为,测试范围更大一些,适合集成测试、系统测试阶段。
三、单元测试编码规范
本文的主要重点是针对日常开发阶段工程师可以做的事情,也就是单元测试而展开。
编写功能、业务代码的时候一般会遵循kiss原则,所以类、方法、函数往往不会太大,分层设计越好、职责越单一、耦合度越低的代码越适合做单元测试,单元测试也倒逼开发过程中代码分层、解耦。
可能某个功能的实现代码有30行,测试代码有50行。单元测试的代码如何编写才更合理、整洁、规范呢?
1.编码分模块展开
先贴一段代码:
- (void)testInsertDataInOneSpecifiedTable
{
XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
// given
[dbInstance removeAllLogsInTableType:PCTLogTableTypeMeta];
NSMutableArray *insertModels = [NSMutableArray array];
for (NSInteger index = 1; index <= 10000; index++) {
PCTLogMetaModel *model = [[PCTLogMetaModel alloc] init];
model.log_id = index;
// ...
[insertModels addObject:model];
}
// when
[dbInstance add:insertModels inTableType:PCTLogTableTypeMeta];
// then
[dbInstance recordsCountInTableType:PCTLogTableTypeMeta completion:^(NSInteger count) {
XCTAssert(count == insertModels.count, @"「数据增加」功能:异常");
[exception fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
|
可以看到这个方法的名称为testInsertDataInOneSpecifiedTable,这段代码做的事情通过函数名可以看出来:测试插入数据到某个特定的表。这个测试用例分为3部分:测试环境所需的先决条件准备;调用所要测试的某个方法、函数;验证输出和行为是否符合预期。
其实,每个测试用例的编写也要按照该种方式去组织代码。步骤分为3个阶段:Given->When->Then。
所以单元测试的代码规范也就出来了。此外单元测试代码规范统一后,每个人的测试代码都按照这个标准展开,那其他人的阅读起来就更加容易、方便。按照这3个步骤去阅读、理解测试代码,就可以清晰明了的知道在做什么。
2.一个测试用例只测试一个分支
我们写的代码有很多语句组成,有各种逻辑判断、分支(if...else、swicth)等等,因此一个程序从一个单一入口进去,过程可能产生n个不同的分支,但是程序的出口总是一个。所以由于这样的特性,我们的测试也需要针对这样的现状走完尽可能多的分支。相应的指标叫做「分支覆盖率」。
假如某个方法内部有if...else...,我们在测试的时候尽量将每种情况写成一个单独的测试用例,单独的输入、输出,判断是否符合预期。这样每个case都单一的测试某个分支,可读性也很高。
比如对下面的函数做单元测试,测试用例设计如下:
- (void)shouldIEatSomething
{
BOOL shouldEat = [self getAteWeight] < self.dailyFoodSupport;
if (shouldEat) {
[self eatSomemuchFood];
} else {
[self doSomeExercise];
}
}
|
- (void)testShouldIEatSomethingWhenHungry
{
// ....
}
- (void)testShouldIEatSomethingWhenFull
{
// ...
}
|
3.明确标识被测试类
这条主要站在团队合作和代码可读性角度出发来说明。写过单元测试的人都知道,可能某个函数本来就10行代码,可是为了测试它,测试代码写了30行。一个方法这样写问题不大,多看看就看明白是在测试哪个类的哪个方法。可是当这个类本身就很大,测试代码很大的情况下,不管是作者自身还是多年后负责维护的其他同事,看这个代码阅读成本会很大,需要先看测试文件名代码类名+Test才知道是测试的是哪个类,看测试方法名test+方法名才知道是测试的是哪个方法。
这样的代码可读性很差,所以应该为当前的测试对象特殊标记,这样测试代码可读性越强、阅读成本越低。比如定义局部变量_sut用来标记当前被测试类(sut,SystemunderTest,软件测试领域有个词叫做被测系统,用来表示正在被测试的系统)。
#import <XCTest/XCTest.h>
#import "PCTLogPayloadModel.h"
@interface PCTLogPayloadModelTest : PCTTestCase
{
PCTLogPayloadModel *_sut;
}
@end
@implementation PCTLogPayloadModelTest
- (void)setUp
{
[super setUp];
PCTLogPayloadModel *model = [[PCTLogPayloadModel alloc] init];
model.log_id = 1;
// ...
_sut = model;
}
- (void)tearDown
{
_sut = nil;
[super tearDown];
}
- (void)testGetDictionary
{
NSDictionary *payloadDictionary = [_sut getDictionary];
XCTAssert([(NSString *)payloadDictionary[@"report_id"] isEqualToString:@"001"] &&
[payloadDictionary[@"size"] integerValue] == 102 &&
[(NSString *)payloadDictionary[@"meta"] containsString:@"meiying"],
@"PCTLogPayloadModel 的 「getDictionary」功能异常");
}
@end
|
4.使用分类来暴露私有方法、私有变量
某些场景下写的测试方法内部可能需要调用被测对象的私有方法,也可能需要访问被测对象的某个私有属性。但是测试类里面是访问不到被测类的私有属性和私有方法的,借助于Category可以实现这样的需求。
为测试类添加一个分类,后缀名为UnitTest。
PrismClient类有私有属性@property(nonatomic,strong)NSString*name;,私有方法-(void)hello。为了在测试用例中访问私有属性和私有方法,写了如下分类:
// PCTPrismClientTest.m
@interface PrismClient (UnitTest)
- (NSString *)name;
- (void)hello;
@end
@implementation PCTPrismClientTest
- (void)testPrivatePropertyAndMethod
{
NSLog(@"%@",[PrismClient sharedInstance].name);
[[PrismClient sharedInstance] hello];
}
@end
|
四、单元测试下开发模式、技术框架选择
单元测试是按照测试范围来划分的。TDD、BDD是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的TDD、BDD方案。
在单元测试阶段,TDD和BDD都可以适用。
1.TDD
TDD强调不断的测试推动代码的开发,这样简化了代码,保证了代码质量。
思想是在拿到一个新的功能时,首先思考该功能如何测试,各种测试用例、各种边界case;然后完成测试代码的开发;最后编写相应的代码以满足、通过这些测试用例。
TDD开发过程类似下图:
先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态;
然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态;
在测试用例的保证下,可以重构、优化代码。
抛出一个问题:TDD看上去很好,应该用它吗?
这个问题不用着急回答,回答了也不会有对错之分。开发中经常是这样一个流程,新的需求出来后,先经过技术评审会议,确定宏观层面的技术方案、确定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即使想的再充分、再细致,可能还是存在特殊case漏掉的情况,导致技术方案或者是技术实现的改变。如果采用TDD,那么之前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。如果遇到了技术方案的变更,之前的测试用例要改变、测试代码实现要改变。可能新增的某个case导致大部分的测试代码和实现代码都要改变。
如何开展TDD
1.新建一个工程,确保“IncludeUnitTests”选项是选中的状态。
2.创建后的工程目录如下。
3.删除Xcode创建的测试模版文件TDDDemoTests.m。
4.假如我们需要设计一个人类,它具有吃饭的功能,且当他吃完后会说一句“好饱啊”。
5.那么按照TDD我们先设计测试用例。假设有个Person类,有个对象方法叫做吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是:
6.实现测试用例代码。创建继承自UnitTestCaseclass的测试类,命名为工程前缀+测试类名+Test,也就是TDDPersonTest.m。
7.因为要测试Person类,所以在主工程中创建Person类。
8.因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在TDDPersonTest.m中创建一个测试函数-(void)testReturnStatusStringWhenPersonAte;函数内容如下:
- (void)testReturnStatusStringWhenPersonAte
{
// Given
Person *somebody = [[Person alloc] init];
// When
NSString *statusMessage = [somebody performSelector:@selector(eat)];
// Then
XCTAssert([statusMessage isEqualToString:@"好饱啊"], @"Person 「吃饭后返回“好饱啊”」功能异常");
}
|
9.Xcode下按快捷键Command+U,跑测试代码发现是失败的。因为我们的Person类根本没实现相应的方法。
10.从TDD开发过程可以看到,我们现在是红色的“Fail”状态。所以需要去Person类中实现功能代码。Person类如下:
#import "Person.h"
@implementation Person
- (NSString *)eat
{
[NSThread sleepForTimeInterval:1];
return @"好饱啊";;
}
@end
|
11.再次运行,跑一下测试用例(Command+U快捷键)。发现测试通过,也就是TDD开发过程中的绿色“Success”状态。
12.例子比较简单,假如情况需要,可以在-(void)setUp方法里面做一些测试的前置准备工作,在-(void)tearDown方法里做资源释放的操作。
13.假如eat方法实现的不够漂亮。现在在测试用例的保证下,大胆重构,最后确保所有的UnitTestcase通过即可。
2.BDD
相比TDD,BDD关注的是行为方式的设计,拿上述“人吃饭”举例说明。
和TDD相比第1~4步骤相同。
5.BDD则需要先实现功能代码。创建Person类,实现-(void)eat;方法。代码和上面的相同。
6.BDD需要引入好用的框架Kiwi,使用Pod的方式引入。
7.因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在TDDPersonTest.m中创建一个测试函数-(void)testReturnStatusStringWhenPersonAte;函数内容如下:
#import "kiwi.h"
#import "Person.h"
SPEC_BEGIN(BDDPersonTest)
describe(@"Person", ^{
context(@"when someone ate", ^{
it(@"should get a string",^{
Person *someone = [[Person alloc] init];
NSString *statusMessage = [someone eat];
[[statusMessage shouldNot] beNil];
[[statusMessage should] equal:@"好饱啊"];
});
});
});
SPEC_END
|
3.XCTest
开发步骤
Xcode自带的测试系统是XCTest,使用简单。开发步骤如下:
在 Tests目录下为被测的类创建一个继承自XCTestCase的测试类。
删除新建的测试代码模版里面的无用方法 -(void)testPerformanceExample、-(void)testExample。
跟普通类一样,可以继承,可以写私有属性、私有方法。所以可以在新建的类里面,根据需求写一些私有属性等。
在-(void)setUp方法里面写一些初始化、启动设置相关的代码。比如测试数据库功能的时候,写一些数据库连接池相关代码。
为被测类里面的每个方法写测试方法。被测类里面可能是n个方法,测试类里面可能是m个方法(m>=n),根据我们在第三部分:单元测试编码规范里讲过的一个测试用例只测试一个分支,方法内部有if、switch语句时,需要为每个分支写测试用例。
为测试类每个方法写的测试方法有一定的规范。命名必须是test+被测方法名。函数无参数、无返回值。比如-(void)testSharedInstance。
测试方法里面的代码按照Given->When->Then的顺序展开。测试环境所需的先决条件准备;调用所要测试的某个方法、函数;使用断言验证输出和行为是否符合预期。
在-(void)tearDown方法里面写一些释放掉资源或者关闭的代码。比如测试数据库功能的时候,写一些数据库连接池关闭的代码。
断言相关宏
/*!
* @function XCTFail(...)
* Generates a failure unconditionally.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTFail(...) \
_XCTPrimitiveFail(self, __VA_ARGS__)
/*!
* @define XCTAssertNil(expression, ...)
* Generates a failure when ((\a expression) != nil).
* @param expression An expression of id type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNil(expression, ...) \
_XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertNotNil(expression, ...)
* Generates a failure when ((\a expression) == nil).
* @param expression An expression of id type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotNil(expression, ...) \
_XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssert(expression, ...)
* Generates a failure when ((\a expression) == false).
* @param expression An expression of boolean type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssert(expression, ...) \
_XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertTrue(expression, ...)
* Generates a failure when ((\a expression) == false).
* @param expression An expression of boolean type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertTrue(expression, ...) \
_XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertFalse(expression, ...)
* Generates a failure when ((\a expression) != false).
* @param expression An expression of boolean type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertFalse(expression, ...) \
_XCTPrimitiveAssertFalse(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertEqualObjects(expression1, expression2, ...)
* Generates a failure when ((\a expression1) not equal to (\a expression2)).
* @param expression1 An expression of id type.
* @param expression2 An expression of id type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualObjects(expression1, expression2, ...) \
_XCTPrimitiveAssertEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertNotEqualObjects(expression1, expression2, ...)
* Generates a failure when ((\a expression1) equal to (\a expression2)).
* @param expression1 An expression of id type.
* @param expression2 An expression of id type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualObjects(expression1, expression2, ...) \
_XCTPrimitiveAssertNotEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertEqual(expression1, expression2, ...)
* Generates a failure when ((\a expression1) != (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertNotEqual(expression1, expression2, ...)
* Generates a failure when ((\a expression1) == (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertNotEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...)
* Generates a failure when (difference between (\a expression1) and (\a expression2) is > (\a accuracy))).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) \
_XCTPrimitiveAssertEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)
/*!
* @define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...)
* Generates a failure when (difference between (\a expression1) and (\a expression2) is <= (\a accuracy)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) \
_XCTPrimitiveAssertNotEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)
/*!
* @define XCTAssertGreaterThan(expression1, expression2, ...)
* Generates a failure when ((\a expression1) <= (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThan(expression1, expression2, ...) \
_XCTPrimitiveAssertGreaterThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertGreaterThanOrEqual(expression1, expression2, ...)
* Generates a failure when ((\a expression1) < (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertGreaterThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertLessThan(expression1, expression2, ...)
* Generates a failure when ((\a expression1) >= (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThan(expression1, expression2, ...) \
_XCTPrimitiveAssertLessThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertLessThanOrEqual(expression1, expression2, ...)
* Generates a failure when ((\a expression1) > (\a expression2)).
* @param expression1 An expression of C scalar type.
* @param expression2 An expression of C scalar type.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThanOrEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertLessThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/*!
* @define XCTAssertThrows(expression, ...)
* Generates a failure when ((\a expression) does not throw).
* @param expression An expression.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrows(expression, ...) \
_XCTPrimitiveAssertThrows(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertThrowsSpecific(expression, exception_class, ...)
* Generates a failure when ((\a expression) does not throw \a exception_class).
* @param expression An expression.
* @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecific(expression, exception_class, ...) \
_XCTPrimitiveAssertThrowsSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)
/*!
* @define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...)
* Generates a failure when ((\a expression) does not throw \a exception_class with \a exception_name).
* @param expression An expression.
* @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
* @param exception_name The name of the exception.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) \
_XCTPrimitiveAssertThrowsSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
/*!
* @define XCTAssertNoThrow(expression, ...)
* Generates a failure when ((\a expression) throws).
* @param expression An expression.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrow(expression, ...) \
_XCTPrimitiveAssertNoThrow(self, expression, @#expression, __VA_ARGS__)
/*!
* @define XCTAssertNoThrowSpecific(expression, exception_class, ...)
* Generates a failure when ((\a expression) throws \a exception_class).
* @param expression An expression.
* @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecific(expression, exception_class, ...) \
_XCTPrimitiveAssertNoThrowSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)
/*!
* @define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...)
* Generates a failure when ((\a expression) throws \a exception_class with \a exception_name).
* @param expression An expression.
* @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
* @param exception_name The name of the exception.
* @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) \
_XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
|
经验小结
1.XCTestCase类和其他类一样,你可以定义基类,这里面封装一些常用的方法。
// PCTTestCase.h
#import <XCTest/XCTest.h>
NS_ASSUME_NONNULL_BEGIN
@interface PCTTestCase : XCTestCase
@property (nonatomic, assign) NSTimeInterval networkTimeout;
/**
用一个默认时间设置异步测试 XCTestExpectation 的超时处理
*/
- (void)waitForExpectationsWithCommonTimeout;
/**
用一个默认时间设置异步测试的
@param handler 超时的处理逻辑
*/
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
/**
生成 Crash 类型的 meta 数据
@return meta 类型的字典
*/
- (NSDictionary *)generateCrashMetaDataFromReport;
@end
NS_ASSUME_NONNULL_END
// PCTTestCase.m
#import "PCTTestCase.h"
#import ...
@implementation PCTTestCase
#pragma mark - life cycle
- (void)setUp
{
[super setUp];
self.networkTimeout = 20.0;
// 1. 设置平台信息
[self setupAppProfile];
// 2. 设置 Mget 配置
[[TITrinityInitManager sharedInstance] setup];
// ....
// 3. 设置 PrismClient
[[PrismClient sharedInstance] setup];
}
- (void)tearDown
{
[super tearDown];
}
#pragma mark - public Method
- (void)waitForExpectationsWithCommonTimeout
{
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
{
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
}
- (NSDictionary *)generateCrashMetaDataFromReport
{
NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
NSDate *crashTime = [NSDate date];
metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
// ...
metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
return [metaDictionary copy];
}
#pragma mark - private method
- (void)setupAppProfile
{
[[CMAppProfile sharedInstance] setMPlatform:@"70"];
// ...
}
@end
|
2.上述说的基本是开发规范相关。测试方法内部如果调用了其他类的方法,则在测试方法内部必须Mock一个外部对象,限制好返回值等。
3.在XCTest内难以使用mock或stub,这些是测试中非常常见且重要的功能。
例子
这里举个例子,是测试一个数据库操作类PCTDatabase,代码只放某个方法的测试代码。
- (void)testRemoveLatestRecordsByCount
{
XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库删除最新数据功能"];
// 1. 先清空数据表
[dbInstance removeAllLogsInTableType:PCTLogTableTypeMeta];
// 2. 再插入一批数据
NSMutableArray *insertModels = [NSMutableArray array];
NSMutableArray *reportIDS = [NSMutableArray array];
for (NSInteger index = 1; index <= 100; index++) {
PCTLogMetaModel *model = [[PCTLogMetaModel alloc] init];
model.log_id = index;
// ...
if (index > 90 && index <= 100) {
[reportIDS addObject:model.report_id];
}
[insertModels addObject:model];
}
[dbInstance add:insertModels inTableType:PCTLogTableTypeMeta];
// 3. 将早期的数据删除掉(id > 90 && id <= 100)
[dbInstance removeLatestRecordsByCount:10 inTableType:PCTLogTableTypeMeta];
// 4. 拿到当前的前10条数据和之前存起来的前10条 id 做比较。再判断当前表中的总记录条数是否等于 90
[dbInstance getLatestRecoreds:10 inTableType:PCTLogTableTypeMeta completion:^(NSArray<PCTLogModel *> * _Nonnull records) {
NSArray<PCTLogModel *> *latestRTentRecords = records;
[dbInstance getOldestRecoreds:100 inTableType:PCTLogTableTypeMeta completion:^(NSArray<PCTLogModel *> * _Nonnull records) {
NSArray<PCTLogModel *> *currentRecords = records;
__block BOOL isEarlyData = NO;
[latestRTentRecords enumerateObjectsUsingBlock:^(PCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([reportIDS containsObject:obj.report_id]) {
isEarlyData = YES;
}
}];
XCTAssert(!isEarlyData && currentRecords.count == 90, @"***Database「删除最新n条数据」功能:异常");
[exception fulfill];
}];
}];
[self waitForExpectationsWithCommonTimeout];
}
|
3.测试框架
1)Kiwi
BDD框架里的Kiwi可圈可点。使用CocoaPods引入pod'Kiwi'。看下面的例子:
被测类(Planck项目是一个基于WebView的SDK,根据业务场景,发现针对WebView的大部分功能定制都是基于WebView的生命周期内发生的,所以参考NodeJS的中间件思想,设计了基于生命周期的WebView中间件):
#import <Foundation/Foundation.h>
@interface TPKTrustListHelper : NSObject
+(void)fetchRemoteTrustList;
+(BOOL)isHostInTrustlist:(NSString *)scheme;
+(NSArray *)trustList;
@end
|
测试类
SPEC_BEGIN(TPKTrustListHelperTest)
describe(@"Middleware Wrapper", ^{
context(@"when get trustlist", ^{
it(@"should get a array of string",^{
NSArray *array = [TPKTrustListHelper trustList];
[[array shouldNot] beNil];
NSString *first = [array firstObject];
[[first shouldNot] beNil];
[[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
});
});
context(@"when check a string wether contained in trustlist ", ^{
it(@"first string should contained in trustlist",^{
NSArray *array = [TPKTrustListHelper trustList];
NSString *first = [array firstObject];
[[theValue([TPKTrustListHelper isHostInTrustlist:first]) should] equal:@(YES)];
});
});
});
SPEC_END
|
例子包含Kiwi的最基础元素。SPEC_BEGIN和SPEC_END表示测试类;describe描述需要被测试的类;context表示一个测试场景,也就是Given->When->Then里的Given;it表示要测试的内容,也就是也就是Given->When->Then里的When和Then。1个describe下可以包含多个context,1个context下可以包含多个it。
Kiwi的使用分为:Specs、Expectations、MocksandStubs、AsynchronousTesting四部分。
it里面的代码块是真正的测试代码,使用链式调用的方式,简单上手。
测试领域中Mock和Stub非常重要。Mock模拟对象可以降低对象之间的依赖,模拟出一个纯净的测试环境(类似初中物理课上“控制变量法”的思想)。Kiwi也支持的非常好,可以模拟对象、模拟空对象、模拟遵循协议的对象等等。Stub存根可以控制某个方法的返回值,这对于方法内调用别的对象的方法返回值很有帮助。减少对于外部的依赖,单一测试当前行为是否符合预期。
针对异步测试,XCTest则需要创建一个XCTestExpectation对象,在异步实现里面调用该对象的fulfill方法,最后设置最大等待时间和完成的回调-(void)waitForExpectationsWithTimeout:(NSTimeInterval)timeouthandler:(nullableXCWaitCompletionHandler)handler;如下例子:
XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
[dbInstance removeAllLogsInTableType:PCTLogTableTypeMeta];
NSMutableArray *insertModels = [NSMutableArray array];
for (NSInteger index = 1; index <= 10000; index++) {
PCTLogMetaModel *model = [[PCTLogMetaModel alloc] init];
model.log_id = index;
// 。。。
[insertModels addObject:model];
}
[dbInstance add:insertModels inTableType:PCTLogTableTypeMeta];
[dbInstance recordsCountInTableType:PCTLogTableTypeMeta completion:^(NSInteger count) {
XCTAssert(count == insertModels.count, @"**Database「数据增加」功能:异常");
[exception fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
|
2)expecta、Specta
expecta和Specta都出自orta之手,他也是Cocoapods的开发者之一。太牛逼了,工程化、质量保证领域的大佬。
Specta是一个轻量级的BDD测试框架,采用DSL模式,让测试更接近于自然语言,因此更易读。
特点:
易于集成到项目中。在Xcode中勾选IncludeUnitTests,和XCTest搭配使用。
语法很规范,对比Kiwi和Specta的文档,发现很多东西都是相同的,也就是很规范,所以学习成本低、后期迁移到其他框架很平滑。
Expecta是一个匹配(断言)框架,相比Xcode的断言XCAssert,Excepta提供更加丰富的断言。
特点:
Eepecta没有数据类型限制,比如1,并不关心是NSInteger还是CGFloat。
链式编程,写起来很舒服。
反向匹配,很灵活。断言匹配用except(...).to.equal(...),断言不匹配则使用.notTo或者.toNot。
延时匹配,可以在链式表达式后加入.will、.willNot、.after(interval)等。
4.小结
Xcode自带的XCTestCase比较适合TDD,不影响源代码,系统独立且不影响App包大小。适合简单场景下的测试。且每个函数在最左侧又个测试按钮,点击后可以单独测试某个函数。
Kiwi是一个强大的BDD框架,适合稍微复杂写的项目,写法舒服、功能强大,模拟对象、存根语法、异步测试等满足几乎所有的测试场景。不能和XCTest继承。
Specta也是一个BDD框架,基于XCTest开发,可以和XCTest模版集合使用。相比Kiwi,Specta轻量一些。开发中一般搭配Excepta使用。如果需要使用Mock和Stud可以搭配OCMock。
Excepta是一个匹配框架,比XCTest的断言则更加全面一些。
没办法说哪个最好、最合理,根据项目需求选择合适的组合。
五、网络测试
我们在测试某个方法的时候可能会遇到方法内部调用了网络通信能力,网络请求成功,可能刷新UI或者给出一些成功的提示;网络失败或者网络不可用则给出一些失败的提示。所以需要对网络通信去看进行模拟。
iOS中很多网络都是基于NSURL系统下的类实现的。所以我们可以利用NSURLProtocol的能力来监控网络并mock网络数据。
开源项目OHHTTPStubs就是一个对网络模拟的库。它可以拦截HTTP请求,返回json数据,定制各种头信息。
Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers! |
几个主要类及其功能:HTTPStubsProtocol拦截网络请求;HTTPStubs单例管理HTTPStubsDescriptor实例对象;HTTPStubsResponse伪造HTTP请求。
HTTPStubsProtocol继承自NSURLProtocol,可以在HTTP请求发送之前对request进行过滤处理:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil);
if (!found && HTTPStubs.sharedInstance.onStubMissingBlock) {
HTTPStubs.sharedInstance.onStubMissingBlock(request);
}
return found;
}
|
firstStubPassingTestForRequest方法内部会判断请求是否需要被当前对象处理。
紧接着开始发送网络请求。实际上在-(void)startLoading方法中可以用任何网络能力去完成请求,比如NSURLSession、NSURLConnection、AFNetworking或其他网络框架。
OHHTTPStubs的做法是获取request、client对象。如果HTTPStubs单例中包含onStubActivationBlock对象,则执行该block,然后利用responseBlock对象返回一个HTTPStubsResponse响应对象。
举个例子,利用Kiwi、OHHTTPStubs测试离线包功能。代码如下:
@interface HORouterManager (Unittest)
- (void)fetchOfflineInfoIfNeeded;
@end
SPEC_BEGIN(HORouterTests)
describe(@"routerTests", ^{
context(@"criticalPath", ^{
__block HORouterManager *routerManager = nil;
beforeAll(^{
routerManager = [[HORouterManager alloc] init];
});
it(@"getLocalPath", ^{
__block NSString *pagePath = nil;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
pagePath = [routerManager filePathOfUrl:@"http://***/resource1"];
});
[[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
__block NSString *rescPath = nil;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
rescPath = [routerManager filePathOfUrl:@"http://***/resource1"];
});
[[expectFutureValue(rescPath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
});
it(@"fetchOffline", ^{
[HOOfflineManager sharedInstance].offlineInfoInterval = 0;
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
return [request.URL.absoluteString containsString:@"h5-offline-pkg"];
} withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"code"] = @(0);
dict[@"data"] = @"f722fc3efce547897819e9449d2ac562cee9075adda79ed74829f9d948e2f6d542a92e969e39dfbbd70aa2a7240d6fa3e51156c067e8685402727b6c13328092ecc0cbc773d95f9e0603b551e9447211b0e3e72648603e3d18e529b128470fa86aeb45d16af967d1a21b3e04361cfc767b7811aec6f19c274d388ddae4c8c68e857c14122a44c92a455051ae001fa7f2b177704bdebf8a2e3277faf0053460e0ecf178549e034a086470fa3bf287abbdd0f79867741293860b8a29590d2c2bb72b749402fb53dfcac95a7744ad21fe7b9e188881d1c24047d58c9fa46b3ebf4bc42a1defc50748758b5624c6c439c182fe21d4190920197628210160cf279187444bd1cb8707362cc4c3ab7486051af088d7851846bea21b64d4a5c73bd69aafc4bb34eb0862d1525c4f9a62ce64308289e2ecbc19ea105aa2bf99af6dd5a3ff653bbe7893adbec37b44a088b0b74b80532c720c79b7bb59fda3daf85b34ef35";
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
return [OHHTTPStubsResponse responseWithData:data
statusCode:200
headers:@{@"Content-Type":@"application/json"}];
}];
[routerManager fetchOfflineInfoIfNeeded];
[[HOOfflineInfo shouldEventually] receive:@selector(saveToLocal:)];
});
});
});
SPEC_END
|
插一嘴,我贴的代码已经好几次可以看到不同的测试框架组合了,所以不是说选了框架A就完事,根据场景选择最优解。
六、UI测试
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物App来说单元测试就不太适合了,如果测试UI界面的正确性、功能是否正确显然就不太适合了。Apple在Xcode7开始推出的UITesting就是苹果自己的UI测试框架。
很多UI自动化测试框架的底层实现都依赖于Accessibility,也就是App可用性。UIAccessibility是iOS3.0引入的一个人性化功能,帮助身体不便的人士方便使用App。
Accessibility通过对UI元素进行分类和标记。分类成类似按钮、文本框、文本等类型,使用identifier来区分不同UI元素。无痕埋点的设计与实现里面也使用accessibilityIdentifier来绑定业务数据。‘
1.使用Xcode自带的UI测试则在创建工程的时候需要勾选“IncludeUITests”。
2.像单元测试意义,UI测试方法命名以test开头。将鼠标光标移到方法内,点击Xcode左下方的红色按钮,开始录制UI脚本。
解释说明:
/*! Proxy for an application that may or may not be running. */
@interface XCUIApplication : XCUIElement
// ...
@end
|
XCUIApplicationlaunch来启动测试。XCUIApplication是UIApplication在测试进程中的代理,用来和App进行一些交互。
使用staticTexts来获取当前屏幕上的静态文本(UILabel)元素的代理。等价于[appdescendantsMatchingType:XCUIElementTypeStaticText]。XCUIElementTypeStaticText参数是枚举类型。
typedef NS_ENUM(NSUInteger, XCUIElementType) {
XCUIElementTypeAny = 0,
XCUIElementTypeOther = 1,
XCUIElementTypeApplication = 2,
XCUIElementTypeGroup = 3,
XCUIElementTypeWindow = 4,
XCUIElementTypeSheet = 5,
XCUIElementTypeDrawer = 6,
XCUIElementTypeAlert = 7,
XCUIElementTypeDialog = 8,
XCUIElementTypeButton = 9,
XCUIElementTypeRadioButton = 10,
XCUIElementTypeRadioGroup = 11,
XCUIElementTypeCheckBox = 12,
XCUIElementTypeDisclosureTriangle = 13,
XCUIElementTypePopUpButton = 14,
XCUIElementTypeComboBox = 15,
XCUIElementTypeMenuButton = 16,
XCUIElementTypeToolbarButton = 17,
XCUIElementTypePopover = 18,
XCUIElementTypeKeyboard = 19,
XCUIElementTypeKey = 20,
XCUIElementTypeNavigationBar = 21,
XCUIElementTypeTabBar = 22,
XCUIElementTypeTabGroup = 23,
XCUIElementTypeToolbar = 24,
XCUIElementTypeStatusBar = 25,
XCUIElementTypeTable = 26,
XCUIElementTypeTableRow = 27,
XCUIElementTypeTableColumn = 28,
XCUIElementTypeOutline = 29,
XCUIElementTypeOutlineRow = 30,
XCUIElementTypeBrowser = 31,
XCUIElementTypeCollectionView = 32,
XCUIElementTypeSlider = 33,
XCUIElementTypePageIndicator = 34,
XCUIElementTypeProgressIndicator = 35,
XCUIElementTypeActivityIndicator = 36,
XCUIElementTypeSegmentedControl = 37,
XCUIElementTypePicker = 38,
XCUIElementTypePickerWheel = 39,
XCUIElementTypeSwitch = 40,
XCUIElementTypeToggle = 41,
XCUIElementTypeLink = 42,
XCUIElementTypeImage = 43,
XCUIElementTypeIcon = 44,
XCUIElementTypeSearchField = 45,
XCUIElementTypeScrollView = 46,
XCUIElementTypeScrollBar = 47,
XCUIElementTypeStaticText = 48,
XCUIElementTypeTextField = 49,
XCUIElementTypeSecureTextField = 50,
XCUIElementTypeDatePicker = 51,
XCUIElementTypeTextView = 52,
XCUIElementTypeMenu = 53,
XCUIElementTypeMenuItem = 54,
XCUIElementTypeMenuBar = 55,
XCUIElementTypeMenuBarItem = 56,
XCUIElementTypeMap = 57,
XCUIElementTypeWebView = 58,
XCUIElementTypeIncrementArrow = 59,
XCUIElementTypeDecrementArrow = 60,
XCUIElementTypeTimeline = 61,
XCUIElementTypeRatingIndicator = 62,
XCUIElementTypeValueIndicator = 63,
XCUIElementTypeSplitGroup = 64,
XCUIElementTypeSplitter = 65,
XCUIElementTypeRelevanceIndicator = 66,
XCUIElementTypeColorWell = 67,
XCUIElementTypeHelpTag = 68,
XCUIElementTypeMatte = 69,
XCUIElementTypeDockItem = 70,
XCUIElementTypeRuler = 71,
XCUIElementTypeRulerMarker = 72,
XCUIElementTypeGrid = 73,
XCUIElementTypeLevelIndicator = 74,
XCUIElementTypeCell = 75,
XCUIElementTypeLayoutArea = 76,
XCUIElementTypeLayoutItem = 77,
XCUIElementTypeHandle = 78,
XCUIElementTypeStepper = 79,
XCUIElementTypeTab = 80,
XCUIElementTypeTouchBar = 81,
XCUIElementTypeStatusItem = 82,
};
|
通过XCUIApplication实例化对象调用descendantsMatchingType:方法得到的是XCUIElementQuery类型。比如@property(readonly,copy*)XCUIElementQuery*staticTexts;
/*! Returns a query for all descendants of the element matching the specified type. */
- (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;
|
descendantsMatchingType返回所有后代的类型匹配对象。childrenMatchingType返回当前层级子元素的类型匹配对象;
/*! Returns a query for direct children of the element matching the specified type. */
- (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;
|
拿到XCUIElementQuery后不能直接拿到XCUIElement。和XCUIApplication类似,XCUIElement不能直接访问UI元素,它是UI元素在测试框架中的代理。可以通过Accessibility中的frame、identifier来获取。
对比很多自动化测试框架都需要找出UI元素,也就是借助于Accessibility的identifier。这里的唯一标识生成对比为UIAutomation添加自动化测试标签的探索]。
七、测试经验总结
TDD写好测试再写业务代码,BDD先写实现代码,再写基于行为的测试代码。另一种思路是没必要针对每个类的私有方法或者每个方法进行测试,因为等全部功能做完后针对每个类的接口测试,一般会覆盖据大多数的方法。等测试完看如果方法未被覆盖,则针对性的补充Unit。
目前,UI测试(appium)还是建议在核心逻辑且长时间没有改动的情况下去做,这样子每次发版本的时候可以当作核心逻辑回归了,目前来看价值是方便后续的迭代和维护上有一些便利性。其他的功能性测试还是走BDD。
对于类、函数、方法的走 TDD,老老实实写UT、走UT覆盖率的把控。
UITesting还是建议在核心逻辑且长时间没有改动的情况下去做,这样子每次发版本的时候可以当作核心逻辑回归,目前来看价值是方便后续的迭代和维护上有一些便利性。例如用户中心SDK升级后,当时有了UITesing,基本上免去了测试人员介入。
如果是一些活动页和逻辑经常变动的,老老实实走测试黑盒...
我觉得一直有个误区,就是觉得自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的。
WWDC这张图也很清楚,UI其实需要的占比较小,还是要靠单测驱动。