测试目的:模拟多种可能性,减少错误,增强健壮性、提高稳定性,减少代码修改导致未知的问题等。
测试种类:
单元测试(Unit Test):
相关框架:Kiwi、Specta
UI测试(UI Test):模拟用户操作,进而从业务层面测试
相关框架:KIF、appium
1.UI比较少则进行单元测试、UI测试
2.UI比较多则进行单元测试,UI测试根据场景决定
单元测试特性
单元性(快速)
测试力度足够小,能够精确定位问题
单一职责:一个测试case只负责一条路径,测试代码中不允许有复杂的逻辑条件
独立性(无依赖)
避免单元测试之间的依赖关系,一个测试的运行不依赖于其他测试代码的运行结果
可重复性(幂等性)
每次执行的结果都相同
自验证
不靠人来检查,必须使用断言
尽可能断言具体的内容(简单的为空判断起不到太大的作用)
测试代码必须有好的前置条件和后置断言
CocoaPods初始化工程推荐的框架为Kiwi、Specta
优点缺点备注
OCMock1、语法与OC类似
2、文档全面
3、学习成本低
1、3.8.1只支持模拟器
2、需要使用<=3.7.1版本
>=3.8.1不支持真机
建议使用<= 3.7.1版本
Specta1、轻松量级、功能简单、链式语法1、文档少、学习成本高
2、如果需要Mock、Stub功能需要引入OCMock
Kiwi1、功能丰富
2、内置Mock、验证等功能
1、文档较少、学习成本较高
2、重量级
XCTestOCMockExpecta
AFNetworking✅
SDWebImage✅✅
FMDB✅✅
MJExtension✅
Pop✅✅
灵活性学习成本迁移成本改造成本
XCTest高低低低
Specta低高高高
Kiwi低高高高
3.1 根据开源框架使用单元测试情况使用最多的是XCTest
3.2 XCTest配合OCMock、Expecta可以实现Kiwi/Specta功能
暂无UI测试,后续补充
1.创建新工程请选中 ☑️Include Tests
2.如果老工程改造File->New->Target->iOS->Unit Testing Bundle
3.集成OCMock: pod 'OCMock', '3.7.1' 使用<=3.7.1版本(>=3.8.1不支持真机) 参考地址:https://ocmock.org/ios/
4.集成Expecta: pod 'Expecta'
```
// person.h
@interface Person : NSObject
- (void)eat;
- (NSInteger)sleep;
@end
// person.m
@interface Person ()
@property(nonatomic, assign) NSInteger age;
@end
@implementation Person
- (void)eat {
NSLog(@"eat");
}
- (NSInteger)sleep {
NSLog(@"sleep");
return 8;
}
- (void)run {
NSLog(@"Person Run");
}
@end
```
一个测试文件只能测试一个类相关功能
测试类以Test结尾,例如:AFImageDownloaderTests
测试方法以Test开头,做到见名知意,例如: testNilProgressDoesNotCauseCrash
写单元测试时不要修改项目源码
每个case只测试一种情况,提高代码可读性,目标测试case中尽量减少if…else…, 如果if…else…太多是不是需要重构代码。
例: Person有两个方法eat和sleep要验证,每个case单独验证一个方法
```
// case1
- (void)testPersonEat {
Person *person = [[Person alloc] init];
[person eat];
}
// case2
- (void)testPersonSleep {
Person *person = [[Person alloc] init];
NSInteger tiem = [person sleep];
XCTAssertEqual(time, 6);
}
```
每个case分为三步: 1.mock对象,准备测试数据 2.调用目标API 3.验证输出和行为
```
- (void)testPersonSleep {
// Given
Person *person = [[Person alloc] init];
// when
NSInteger tiem = [person sleep];
// then
XCTAssertEqual(time, 6);
}
```
当一个case想要访问私有方法或者私有属性时就需要被访问类公开属性或方法,我们要避免访问私有属性或方法,在迫不得已情况下可以通过在测试文件开头用一个category来暴露被访问类的私有属性或者方法。
访问person中的私有方法run和私有属性age,可以参考一下代码
```
// person.h
@interface Person : NSObject
@end
// person.m
@interface Person ()
@property(nonatomic, assign) NSInteger age;
@end
@implementation Person
- (void)run {
NSLog(@"Person Run");
}
@end
// UITestDemoTests.m
@interface Person (UITestDemoTests)
- (void)run;
@property(nonatomic, assign) NSInteger age;
@end
@interface UITestDemoTests : XCTestCase
- (void)testPrivateProperty {
Person *person = [[Person alloc] init];
person.age = 100;
XCTAssertEqual(person.age, 100);
}
- (void)testPrivateMethod {
Person *person = [[Person alloc] init];
[person run];
}
@end
```
【强制】代码编写前做好代码分层、隔离设计,为后续单元测试做准备。
【强制】核心应用核心业务增量代码一定要写单元测试。
【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之 间决不能互相调用,也不能依赖执行的先后次序。 反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入
【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类 级别,一般是方法级别。 说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑, 那是集成测试的领域。
【强制】暴露给外部使用的API必须进行单元测试且通过。 说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
【强制】单元测试代码不允许写在业务代码目录下。
【强制】单元测试类的命名应该和被测试类保持一致xxxTest,测试方法为被测方法名textxxx。
【强制】开发必须保证自己写的单元测试能在本地执行通过,才能提交。
【强制】重视边界值测试,充分考虑边界和异常,核心业务模块的代码保证尽量高的代码测试覆盖率。
【强制】不需要的单元测试直接删除,不要注释掉,如果非要注释请写清楚注释理由。
【强制】对于外部第三方SDK要单独进行封装隔离,方便后续单元测试和更换。
【推荐】对于依赖本应用之外的所有第三方环境的单元测试,建议使用Mock的方式进行测试,尽量将正常/异常情况模拟到,即做到尽可能的摆脱对环境依赖、持续重复运行。
【推荐】写测试代码的目的是为了提高业务代码质量,严禁为达到测试要求而书写不规范测试代码;对于不可测的代码建议做必要的重构,使代码变的可测。
【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C:Correct,正确的输入,并得到预期的结果。
D:Design,与设计文档相结合,来编写单元测试。
E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
【推荐】对于不可测的代码在适当的时机做必要的重构,使代码变得可测,避免为了达到测 试要求而书写不规范测试代码。
【推荐】单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后 补充单元测试用例。
【推荐】所有请求服务端的接口全部单独封装做到可支持单元测试针对测试。
理想:覆盖所有类、函数、逻辑
实际:至少保证暴露给外部的类和逻辑应该测试到
测试重点:
尽量避免测试类的私有成员/方法,它让测试变得繁琐而且更难维护。
如果有私有成员确实需要进行直接测试,可以考虑把它重构到工具类的公有方法中,但要注意这么做是为了改善设计, 而不是帮助测试.
接口较为复杂,含有较多的逻辑分支时,单元测试应尽可能测试到所有的业务逻辑
对于每个Service都要进行单元测试
单元测试Mock数据要模拟正常/异常情况,客户端各种可能出现的情况都要覆盖到
苹果相关权限如:网络、定位、通知等
单元测试要模拟已/未获取相关权限
变量是否有初始值或在某些场景下是否有默认值
变量、数组、字典等的溢出越界等
注意边界和异常值
变量无赋值(null)
变量是数值或字符
主要边界:最大值,最小值,无穷大
溢出边界:在边界外面取值+/-1
临近边界:在边界值之内取值+/-1
字符串的设置,空字符串
目标集合的类型和应用边界
变量是规律的,测试无穷大的极限,无穷小的极限
数组越界
字典设置空值
多线程操作同一变量
对于涉及到业务逻辑的异常,需要覆盖
对于封装第三方的SDK虽然没有暴露给外部,但要保证第三方SDK所有场景覆盖
对于有些场景可能在弱网下出问题,需要模拟弱网情况
对于前后台切换可能影响业务的场景要覆盖到
有外部数据依赖时尽量使用Mock进行数据模拟
公有接口
重要复杂的算法、逻辑、功能以及容易出错的分支
请求服务端的接口
苹果相关权限未/开启情况下
局部数据结构测试
边界条件测试
正常/异常情况
所有独立代码测试
弱网测试
前后台切换测试
单例相关测试
有外部依赖时使用Mock
后续补充
TDD:在开发功能代码之前,先编写单元测试用例代码,测试代码驱动产品代码开发
边开发边单测:每个功能点,先写产品代码,再写单测代码
先开发再单测:先完成所有或部分功能点,在提测前补单测
TDD做好比较难,存在争议也比较多;网络上推荐建议采用边开发边单测的方式。
但是客户端通常采用先开发再单元测试。
注意不要导致内存泄漏,可使用XCode或者MLeaksFinder自行检测
对于复杂UI需要检测帧率是否达标,检测设备以6s设备为准,不允许使用6s以上设备检测
6s性能较差(检测设备可以根据设备分布情况调整)
暂时不考虑
暂时不考虑
如果页面帧率不达标请使用XCode检测渲染卡顿问题
如果页面需要请求多个接口,请对页面加载时间进行单元测试,如果不达可能需要考虑重构页面或接口
针对复杂页面请对加载时间进行单元测试
所有请求网络的接口都要进行单元测试,以保证接口正确性和稳定性。
对于有些接口可能依赖外部参数或者数据,请自行创造测试条件。
暂时不考虑
代码覆盖率统计是用来发现没有被测试覆盖的代码;代码覆盖率统计不能完全用来衡量代码质量。
分析微覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。
Xcode为我们提供覆盖检测, 使用流程
Edit Scheme - Test - Options - 勾选 Gather coverage for (全部文件, 或者自定义部分文件)
然后command+u 跑测试用例之后便可在如图所示,查阅单元测试的覆盖率
```
XCTFail(format…) 生成一个失败的测试;
XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;
XCTAssert(expression, format...)当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)当expression求值为TRUE时通过;
XCTAssertFalse(expression, format...)当expression求值为False时通过;
XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用, 判断的是变量的地址,如果地址相同则返回TRUE,否则返回NO);
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试; XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过; XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过; XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrowSpecific(expression, specificException, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过; XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
```
What Mock?
Mock就是做一个假的object,对这个object里的方法的调用,都会被Mockito拦截,然后返回用户预设的行为。这样可以绕过需要从其它地方拿数据的地方,直接返回用户预设的数据,进行单元测试。
When mock?
1.其它的协同模块尚未开发完成
2.被测试模块需要和一些不容易构造、比较复杂的对象进行交互
3.由于不能肯定其它模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的
4.网络交互,外部系统调用接口,如果两个被测模块之间是通过网络进行交互的
Why use mock?
我们可以使用苹果的Runtime来实现Mock,但是需要自己开发太多基础东西,所以引入Mock框架
Mock: 创建一个模拟对象实例,我们可以验证,修改它的行为
Stub: Mock对象的函数返回特定的值
Partial Mock: 重写Mock对象的方法
需要注意的是mock的对象在用例结束后要stopMocking,避免由于单例或者property导致的用例之间相互影响
```
id classMock = OCMClassMock([SomeClass class]);
```
Creates a mock object that can be used as if it were an instance of SomeClass. It is possible to mock instance and class methods defined in the class and its superclasses.
There are some subtleties when mocking class methods. See mocking class methods below.
🌰例子:
```
id mock = OCMClassMock([NSString class]);
```
```
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
```
Creates a mock object that can be used as if it were an instance of an object that implements SomeProtocol. Otherwise they work like class mocks.
🌰例子:
```
// 辅助类
@protocol TestProtocol
+ (NSString *)stringValueClassMethod;
- (int)primitiveValue;
@optional
- (id)objectValue;
- (void)voidWithArgument:(id)argument;
@end
// demo1
- (void)testProtocolClassMethod {
id mock = OCMProtocolMock(@protocol(TestProtocol));
OCMStub([mock stringValueClassMethod]).andReturn(@"stubbed");
id result = [mock stringValueClassMethod];
XCTAssertEqual(@"stubbed", result, @"Should have stubbed the class method.");
}
// demo2
- (void)testRefusesToCreateProtocolMockForNilProtocol {
XCTAssertThrows(OCMProtocolMock(nil));
}
// demo3
- (void)testArgumentsGetReleasedAfterStopMocking {
__weak id weakArgument;
id mock = OCMProtocolMock(@protocol(TestProtocol));
@autoreleasepool {
NSObject *argument = [NSObject new];
weakArgument = argument;
[mock voidWithArgument:argument];
[mock stopMocking];
}
XCTAssertNil(weakArgument);
}
```
1.3 Strict class and protocol mocks
```
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
```
Creates a mock object in strict mode. By default mocks are nice, they return nil (or the correct default value for the return type) for whatever method is called. In contrast, strict mocks raise an exception when they receive a method that was not explicitly expected. See strict mocks and expectations below.
🌰例子:
```
// demo1
- (void)testSetsUpStubsForCorrectMethods {
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock uppercaseString]).andReturn(@"TEST_STRING");
XCTAssertEqualObjects(@"TEST_STRING", [mock uppercaseString], @"Should have returned stubbed value");
XCTAssertThrows([mock lowercaseString]);
}
// demo2
- (void)testSetsUpStubsWithNonObjectReturnValues {
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock boolValue]).andReturn(YES);
XCTAssertEqual(YES, [mock boolValue], @"Should have returned stubbed value");
}
// demo3
- (void)testCanUseVariablesInInvocationSpec {
id mock = OCMStrictClassMock([NSString class]);
NSString *expected = @"foo";
OCMStub([mock rangeOfString:expected]).andReturn(NSMakeRange(0, 3));
XCTAssertThrows([mock rangeOfString:@"bar"], @"Should not have accepted invocation with non-matching arg.");
}
// demo4
- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgument {
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs();
XCTAssertNoThrow([mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]);
}
```
`id partialMock = OCMPartialMock(anObject);`
Creates a mock object that can be used in the same way as anObject. Any method that is not stubbed is forwarded to anObject. When a method is stubbed and that method is invoked using a reference to the real object, the mock will still be able to handle the invocation. Similarly, when a method is invoked using a reference to anObject, rather than the mock, it can still be verified later.
There are some subtleties when using partial mocks. Seepartial mocksbelow.
🌰例子:
```
// demo1
- (void)testSetsUpStubReturningNilForIdReturnType {
id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]);
OCMStub([mock lastObject]).andReturn(nil);
XCTAssertNil([mock lastObject], @"Should have returned stubbed value");
}
// demo2
@interface TestClassWithClassReturnMethod : NSObject
- (Class)method;
@end
@implementation TestClassWithClassReturnMethod
- (Class)method {
return [self class];
}
@end
// demo2
- (void)testSetsUpStubReturningNilForClassReturnType {
id mock = OCMPartialMock([[TestClassWithClassReturnMethod alloc] init]);
OCMStub([mock method]).andReturn(Nil);
XCTAssertNil([mock method], @"Should have returned stubbed value");
// sometimes nil is used where Nil should be used
OCMStub([mock method]).andReturn(nil);
XCTAssertNil([mock method], @"Should have returned stubbed value");
}
```
Observer mocks are deprecated as of OCMock 3.8. Please use XCTNSNotificationExpectation instead.
`id observerMock = OCMObserverMock();`
Creates a mock object that can be used to observe notifications. The mock must be registered in order to receive notifications. Seeobserver mocksbelow for details.
🌰例子:
```
// 辅助类
@protocol TestProtocolForMacroTesting
- (NSString *)stringValue;
@end
// demo1
- (void)testSetsUpNotificationPostingAndNotificationObserving {
id mock = OCMProtocolMock(@protocol(TestProtocolForMacroTesting));
NSNotification *n = [NSNotification notificationWithName:@"TestNotification" object:nil];
id observer = OCMObserverMock();
[[NSNotificationCenter defaultCenter] addMockObserver:observer name:[n name] object:nil];
OCMExpect([observer notificationWithName:[n name] object:[OCMArg any]]);
OCMStub([mock stringValue]).andPost(n);
[mock stringValue];
OCMVerifyAll(observer);
}
// demo2
- (void)testNotificationObservingWithUserInfo {
id observer = OCMObserverMock();
[[NSNotificationCenter defaultCenter] addMockObserver:observer name:@"TestNotificationWithInfo" object:nil];
OCMExpect([observer notificationWithName:@"TestNotificationWithInfo" object:[OCMArg any] userInfo:[OCMArg any]]);
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotificationWithInfo" object:self userInfo:@{ @"foo": @"bar" }];
OCMVerifyAll(observer);
}
```
Why use Expecta?
XCTest中的断言可能不能满足日常开发需求,所以引入断言框架,不强制使用请根据需要自行决定
......
1.https://juejin.cn/post/6844904138388537352#heading-9
2.https://github.com/zhuifengshen/SwiftUnitTestsSamples
3.https://github.com/cyneck/unit-test-specification
4.https://www.cnblogs.com/M-Silencer/p/11215065.html