0x01 介绍
Kiwi是一个适用于iOS开发的行为驱动测试框架,旨在提供一个足够简单易用的BDD(Behavior Driven Development)库.
0x02 安装
使用Cocoapods安装,在测试Target中增加以下配置:
pod 'Kiwi', '3.0.0'
0x03 基本使用
先看一个完整的代码示例:
#import <Kiwi/Kiwi.h>
SPEC_BEGIN(KiwiTest)
describe(@"descibeString", ^{
context(@"contextString", ^{
beforeAll(^{
NSLog(@"beforeAll");
});
afterAll(^{
NSLog(@"afterAll");
});
beforeEach(^{
NSLog(@"beforeEach");
});
afterEach(^{
NSLog(@"afterEach");
});
it(@"test1", ^{
[[theValue(1) should] equal:@(1)];
NSLog(@"test1");
});
it(@"test2", ^{
[[theValue(2) should] equal:@(2)];
NSLog(@"test2");
});
});
});
SPEC_END
使用Kiwi编写的测试用例不会在用例开发结束后就显示在xcode测试用例列表中,必须要运行一遍测试用例后才会显示出来(不管成功还是失败),这一点是不如XCTest直观的。
测试用例运行成功后,会生成2条测试用例,见下图:
<img src='https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12269e5e50b04a29b9f394a65760b382~tplv-k3u1fbpfcp-watermark.image?' width=460 height=80 />
日志打印如下图:
<img src='https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6750a7417fc49d7a8ec319a2f6a30cd~tplv-k3u1fbpfcp-watermark.image?' width=640 height=320 />
通过日志输出,基本可以看出API间的等价关系:
Kiwi | XCTest |
---|---|
beforeAll() |
setUp() |
afterAll() |
tearDown() |
it() |
testXXX() |
[[theValue(xxx) should] equal:yyy] |
XCTAssertEqual(xx, yyy, @"") |
同时Kiwi还提供了2个新的方法,beforeEach
和afterEach
,分别在每个测试用例开始执行前和执行结束后触发,方便在每个测试用例执行前后做一些初始化和清理方面的事情。
0x04 Kiwi 常用 API 介绍
Kiwi
提供了丰富的逻辑判断API,用法如下:
1. bool判断
[[theValue(boolVar) should] beYes]
[[theValue(boolVar) shouldNot] beYes]
[[theValue(boolVar) should] beNo]
[[theValue(boolVar) shouldNot] beNo]
等价于 XCTAssertTrue(boolVar)
以及 XCTAssertFalse(boolVar)
。
2. 值判断
[[theValue(1) should] equal:@(1)]
[[@"string1" shouldNot] equal:@"string"]
等价于 XCTAssertEqual
和 XCTAssertNotEqual
。
3. 空判断
[[oc_object should] beNil];
[[oc_object shouldNot] beNil];
等价于 XCTAssertNil
和 XCTAssertNotNil
。
上述API,和XCTest用法一样,不同在写法上,XCTest更像是C语言的API形式,Kiwi更像是OC的API形式,语义上更直白。
4. 延时判断
expectFutureValue
这个API很有用,通常用在异步场景中,比如网络请求、定时器等,同时写在延时判断后面的测试用例又成为同步判断。
// 30秒后,result值在应该是YES
[[expectFutureValue(@(result)) shouldAfterWaitOf(30.0)] equal:@(YES)];
例如:
__block BOOL result = NO;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 做一些很耗时的操作
result = YES;
});
// 10秒后判断result值是否是YES
[[expectFutureValue(@(result)) shouldAfterWaitOf(10.0)] equal:@(YES)];
// 在上一个判断结束后,再判断下面的用例
[[@"string1" shouldNot] equal:@"string"]
0x05 Kiwi进阶用法
1. Stub
用于替换指定方法的执行过程或者返回结果,Kiwi
提供了如下丰富的stub API
API 介绍
- (void)stub:(SEL)aSelector;
- (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;
- (void)stub:(SEL)aSelector withArguments:(id)firstArgument, ...;
- (void)stub:(SEL)aSelector andReturn:(id)aValue;
- (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...;
- (void)stub:(SEL)aSelector andReturn:(id)aValue times:(NSNumber *)times afterThatReturn:(id)aSecondValue;
+ (void)stub:(SEL)aSelector;
+ (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;
+ (void)stub:(SEL)aSelector withArguments:(id)firstArgument, ...;
+ (void)stub:(SEL)aSelector andReturn:(id)aValue;
+ (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...;
+ (void)stub:(SEL)aSelector andReturn:(id)aValue times:(NSNumber *)times afterThatReturn:(id)aSecondValue;
仔细观看,不难发现,上述API分2类,一类是实例方法,一类是类方法。这里挑几个常用的说明下
-
- (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;
当调用aSelector
时,不走原来的逻辑,而是执行你传递的block
。 -
- (void)stub:(SEL)aSelector andReturn:(id)aValue
当调用aSelector
时,不返回原来的值,而是用你传递的值来返回 -
- (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...
当调用aSelector
并且参数是你指定的参数时,不返回原来的值,而是用你传递的值来返回。
使用场景
比如你要测试一个复杂的方法A,这个方法中调用了其他模块的一些方法(B、C、D),而方法A中会根据B、C、D方法的不同返回值做不同的逻辑处理,那么你要怎么写测试用例,来验证方法A在不同的逻辑分支处理结果都满足预期呢?
- 这种场景在XCTest中是不好处理的,因为你没法处理A方法的内部细节,对写测试用例的人来说,相当于一个黑盒。
- 而在Kiwi中,只要在调用A方法前,对B、C、D分别调用相应的stub方法,之后再调用A方法,A方法内的B、C、D方法就能按照之前stub的约定执行指定的block和返回指定的值。
示例如下:
@implementation Runner
- (BOOL)start {
// 返回值由当前设备环境以及登录账号来决定
BOOL enabled = [ConfigABTest enabled];
if (enable) {
return YES;
}
return NO;
}
@end
我们需要测试[Runner.instance start]
这个方法在不同场景下是否返回正确的值,
而在此方法内部需要调用[ConfigABTest enabled]
方法。比如在我当前测试设备或者测试账号中,
[ConfigABTest enabled]
只能返回YES,XCTest
中的测试用例在不更换测试设备或者账号的前提下,
就没法验证[ConfigABTest enabled]
在返回NO
的情况下,[Runner.instance start]
是否也返回正确的值。
而在Kiwi中,这很容易做到,只需要编写如下测试代码就可以了
// 测试YES的场景
[ConfigABTest stub:@selector(enabled) andReturn:theValue(YES)];
BOOL result = [Runner.instance start];
[[theValue(result) should] beYes];
// 测试NO的场景
[ConfigABTest stub:@selector(enabled) andReturn:theValue(NO)];
BOOL result = [Runner.instance start];
[[theValue(result) should] beNo];
2. Mock
这个比较好理解,在网络请求场景中,经常需要mock数据来验证服务端返回各种数据时,客户端是否都能正确处理。那么在Kiwi中,mock一个对象就是生成一个源对象几乎行为一致的对象。
API介绍
// NSOject + KiwiMockAdditions.h
/// mock调用的类
+ (id)mock;
// KWMock.h
/// mock指定class并生成一个对象
+ (id)mockForClass:(Class)aClass;
/// mock一个遵循指定协议的对象
+ (id)mockForProtocol:(Protocol *)aProtocol;
/// 根据指定的object生成一个mock了该object类型的对象
+ (id)partialMockForObject:(id)object;
使用场景
比如现在需要针对下面的[MyDownloader downloadWithURL:handler:]
方法写测试用例,
@protocol MyDownloadDelegate <NSObject>
@required
/// 通知下载结果
- (void)downloadComplete:(NSString * _Nullable)dstPath error:(NSError * _Nullable)error;
@end
@implementation MyDownloader
- (void)downloadWithURL:(NSString *)url handler:(id<MyDownloadDelegate>)delegate {
// 通过调用底层网络库处理下载
[XXXNetwork downloadWithUrl:url completeHandler:^(NSString *path, NSError *error) {
// 对path做一些转换
// 对error做一些提示上的转换
[delegate downloadComplete:path error:error];
}];
}
@end
因为[MyDownloader downloadWithURL:handler:]
需要一个protocol
参数,我不可能为了这个参数去现场定义一个类实现指定的protocol
,这种情况下就轮到mock
上场了。
it(@"测试下载", ^{
AnyDeclaredClass *mocked = [KWMock mockForProtocol:@protocol(MyDownloadDelegate)];
[mocked stub:@selector(downloadComplete:error:) withBlock:^id(NSArray *params) {
resultImage = [params[0] isEqual:[NSNull null]] ? nil : params[0];
resultError = [params[1] isEqual:[NSNull null]] ? nil : params[1];
return nil;
}];
MyDownloader *downloader = MyDownloader alloc] init];
[downloader downloadWithURL:@"" handler:mockDelegate];
});
当执行到[MyDownloader downloadWithURL:handler:]
中的[delegate downloadComplete:path error:error];
这一行代码时,就会执行到上面[mocked stub:withBlcok:]
中指定的block
体中去,这样就避免了创建新类的操作。
0x06 个人体会
个人觉得编写测试用例最大的好处,就是能更早的发现问题。要知道问题发现的越晚,为改正它而花费的代价就越大。但是我们为什么就坚持不下去呢?最主要的原因就是随着App的规模不断增长,XCTest
测试框架能应用的场景越来越少,起到的作用很有限。比如业务场景的数据都是通过网络请求去获取的,XCTest
没法切入进去定制化响应数据以验证我们代码的健壮性;还有我们的模块代码内调用其他模块的代码,我们没法验证其他模块代码返回不同值时,我们的模块代码是否都能够正确处理,等等。
现在Kiwi提供了强大的Stub
和Mock
能力,上面所说的困难也都不存在了。自从使用Kiwi
开发测试用例后,每次迭代只要测试用例都能跑通,上线基本上是没有问题的,就算上线发现了问题,只要及时补充用例,也能杜绝相同的问题再次出现的情况。
0x07 参考资料
https://cloud.tencent.com/developer/article/1011286
https://cloud.tencent.com/developer/article/1972234 (建议详读)
网友评论