目录:
四、 单元测试下开发模式、技术框架选择
单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的 TDD、BDD 方案。
在单元测试阶段,TDD 和 BDD 都可以适用。
1. TDD
TDD 强调不断的测试推动代码的开发,这样简化了
代码,保证了代码质量。
思想是在拿到一个新的功能时,首先思考该功能如何测试,各种测试用例、各种边界 case;然后完成测试代码的开发;最后编写相应的代码以满足、通过这些测试用例。
TDD 开发过程类似下图:
- 先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态
- 然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态
- 在测试用例的保证下,可以重构、优化代码
抛出一个问题:TDD 看上去很好,应该用它吗?
这个问题不用着急回答,回答了也不会有对错之分。开发中经常是这样一个流程,新的需求出来后,先经过技术评审会议,确定宏观层面的技术方案、确定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即使想的再充分、再细致,可能还是存在特殊 case 漏掉的情况,导致技术方案或者是技术实现的改变。如果采用 TDD,那么之前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。如果遇到了技术方案的变更,之前的测试用例要改变、测试代码实现要改变。可能新增的某个 case 导致大部分的测试代码和实现代码都要改变。
如何开展 TDD**
-
新建一个工程,确保 “Include Unit Tests” 选项是选中的状态
-
创建后的工程目录如下
-
删除 Xcode 创建的测试模版文件
TDDDemoTests.m
-
假如我们需要设计一个人类,它具有吃饭的功能,且当他吃完后会说一句“好饱啊”。
-
那么按照 TDD 我们先设计测试用例。假设有个 Person 类,有个对象方法叫做吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是
步骤 期望 结果 实例化 Person 对象,调用对象的 eat 方法 调用后返回“好饱啊” ? -
实现测试用例代码。创建继承自 Unit Test Case class 的测试类,命名为
工程前缀+测试类名+Test
,也就是TDDPersonTest.m
。【自动化交流群】:1140267353 本群提供免费的学习指导,自动化资料以及免费的解答,不懂得问题都可以在本群提出来,之后还会有职业生涯规划以及面试指导; -
因为要测试 Person 类,所以在主工程中创建 Person 类
-
因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在
TDDPersonTest.m
中创建一个测试函数-(void)testReturnStatusStringWhenPersonAte;
函数内容如下- (void)testReturnStatusStringWhenPersonAte { // Given Person *somebody = [[Person alloc] init]; // When NSString *statusMessage = [somebody performSelector:@selector(eat)]; // Then XCTAssert([statusMessage isEqualToString:@"好饱啊"], @"Person 「吃饭后返回“好饱啊”」功能异常"); } 复制代码
-
Xcode 下按快捷键
Command + U
,跑测试代码发现是失败的。因为我们的 Person 类根本没实现相应的方法 -
从 TDD 开发过程可以看到,我们现在是红色的 “Fail” 状态。所以需要去 Person 类中实现功能代码。Person 类如下
#import "Person.h" @implementation Person - (NSString *)eat { [NSThread sleepForTimeInterval:1]; return @"好饱啊";; } @end 复制代码
-
再次运行,跑一下测试用例(Command + U 快捷键)。发现测试通过,也就是TDD 开发过程中的绿色 “Success” 状态。
-
例子比较简单,假如情况需要,可以在
-(void)setUp
方法里面做一些测试的前置准备工作,在-(void)tearDown
方法里做资源释放的操作 -
假如 eat 方法实现的不够漂亮。现在在测试用例的保证下,大胆重构,最后确保所有的 Unit Test case 通过即可。
2. BDD
相比 TDD,BDD 关注的是行为方式的设计,拿上述“人吃饭”举例说明。
和 TDD 相比第1~4步骤相同。
-
BDD 则需要先实现功能代码。创建 Person 类,实现
-(void)eat;
方法。代码和上面的相同 -
BDD 需要引入好用的框架
Kiwi
,使用 Pod 的方式引入 -
因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在
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__)
复制代码
经验小结
-
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 复制代码
-
上述说的基本是开发规范相关。测试方法内部如果调用了其他类的方法,则在测试方法内部必须 Mock 一个外部对象,限制好返回值等。
-
在 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'
。看下面的例子【自动化交流群】:1140267353 本群提供免费的学习指导,自动化资料以及免费的解答,不懂得问题都可以在本群提出来,之后还会有职业生涯规划以及面试指导;
被测类(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 、 Mocks and Stubs 、Asynchronous Testing 四部分。点击可以访问详细的说明文档。
it
里面的代码块是真正的测试代码,使用链式调用的方式,简单上手。
测试领域中 Mock 和 Stub 非常重要。Mock 模拟对象可以降低对象之间的依赖,模拟出一个纯净的测试环境(类似初中物理课上“控制变量法”的思想)。Kiwi 也支持的非常好,可以模拟对象、模拟空对象、模拟遵循协议的对象等等,点击 Mocks and Stubs 查看。Stub 存根可以控制某个方法的返回值,这对于方法内调用别的对象的方法返回值很有帮助。减少对于外部的依赖,单一测试当前行为是否符合预期。
针对异步测试,XCTest 则需要创建一个 XCTestExpectation
对象,在异步实现里面调用该对象的 fulfill
方法,最后设置最大等待时间和完成的回调 - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)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 中勾选
Include Unit Tests
,和 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 的断言则更加全面一些。
没办法说哪个最好、最合理,根据项目需求选择合适的组合。
下一篇 写好测试,提升应用质量。涨薪分分钟!!!(三)
点个关注不迷路
小枫文章整理不易,欢迎各位朋友点赞关注