来源:http://www.51testing.com
四、 单元测试下开发模式、技术框架选择
单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的 TDD、BDD 方案。
在单元测试阶段,TDD 和 BDD 都可以适用。
1. TDD
TDD 强调不断的测试推动代码的开发,这样简化了代码,保证了代码质量。
思想是在拿到一个新的功能时,首先思考该功能如何测试,各种测试用例、各种边界 case;然后完成测试代码的开发;最后编写相应的代码以满足、通过这些测试用例。
TDD 开发过程类似下图:
先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态。
然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态。
在测试用例的保证下,可以重构、优化代码。
抛出一个问题:TDD 看上去很好,应该用它吗?
这个问题不用着急回答,回答了也不会有对错之分。开发中经常是这样一个流程,新的需求出来后,先经过技术评审会议,确定宏观层面的技术方案、确定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即使想的再充分、再细致,可能还是存在特殊 case 漏掉的情况,导致技术方案或者是技术实现的改变。如果采用 TDD,那么之前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。如果遇到了技术方案的变更,之前的测试用例要改变、测试代码实现要改变。可能新增的某个 case 导致大部分的测试代码和实现代码都要改变。
如何开展 TDD
新建一个工程,确保 “Include Unit Tests” 选项是选中的状态。
创建后的工程目录如下:
删除 Xcode 创建的测试模版文件 TDDDemoTests.m。
假如我们需要设计一个人类,它具有吃饭的功能,且当他吃完后会说一句“好饱啊”。
那么按照 TDD 我们先设计测试用例。假设有个 Person 类,有个对象方法叫做吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是:
实现测试用例代码。创建继承自 Unit Test Case class 的测试类,命名为 工程前缀 测试类名 Test,也就是 TDDPersonTest.m。
因为要测试 Person 类,所以在主工程中创建 Person 类。
因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在 TDDPersonTest.m 中创建一个测试函数 -
代码语言:javascript复制(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 类如下:
代码语言:javascript复制#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 中创建一个测试函数 -
代码语言:javascript复制(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_EN
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 方法里面写一些释放掉资源或者关闭的代码。比如测试数据库功能的时候,写一些数据库连接池关闭的代码。
断言相关宏:
代码语言:javascript复制/*!
* @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 类和其他类一样,你可以定义基类,这里面封装一些常用的方法。
代码语言:javascript复制// HCTTestCase.h
#import <XCTest/XCTest.h>
NS_ASSUME_NONNULL_BEGIN
@interface HCTTestCase : 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
// HCTTestCase.m
#import "HCTTestCase.h"
#import ...
@implementation HCTTestCase
#pragma mark - life cycle
- (void)setUp
{
[super setUp];
self.networkTimeout = 20.0;
// 1. 设置平台信息
[self setupAppProfile];
// 2. 设置 Mget 配置
[[TITrinityInitManager sharedInstance] setup];
// ....
// 3. 设置 HermesClient
[[HermesClient 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,这些是测试中非常常见且重要的功能。
例子
这里举个例子,是测试一个数据库操作类 HCTDatabase,代码只放某个方法的测试代码。
代码语言:javascript复制- (void)testRemoveLatestRecordsByCount
{
XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库删除最新数据功能"];
// 1. 先清空数据表
[dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
// 2. 再插入一批数据
NSMutableArray *insertModels = [NSMutableArray array];
NSMutableArray *reportIDS = [NSMutableArray array];
for (NSInteger index = 1; index <= 100; index ) {
HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
model.log_id = index;
// ...
if (index > 90 && index <= 100) {
[reportIDS addObject:model.report_id];
}
[insertModels addObject:model];
}
[dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
// 3. 将早期的数据删除掉(id > 90 && id <= 100)
[dbInstance removeLatestRecordsByCount:10 inTableType:HCTLogTableTypeMeta];
// 4. 拿到当前的前10条数据和之前存起来的前10条 id 做比较。再判断当前表中的总记录条数是否等于 90
[dbInstance getLatestRecoreds:10 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
NSArray<HCTLogModel *> *latestRTentRecords = records;
[dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
NSArray<HCTLogModel *> *currentRecords = records;
__block BOOL isEarlyData = NO;
[latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _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 中间件)。
代码语言:javascript复制#import <Foundation/Foundation.h>
@interface TPKTrustListHelper : NSObject
(void)fetchRemoteTrustList;
(BOOL)isHostInTrustlist:(NSString *)scheme;
(NSArray *)trustList;
@end
测试类:
代码语言:javascript复制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; 如下例子:
代码语言:javascript复制XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
[dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
NSMutableArray *insertModels = [NSMutableArray array];
for (NSInteger index = 1; index <= 10000; index ) {
HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
model.log_id = index;
// 。。。
[insertModels addObject:model];
}
[dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
[dbInstance recordsCountInTableType:HCTLogTableTypeMeta 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 的断言则更加全面一些。
没办法说哪个最好、最合理,根据项目需求选择合适的组合。