1、什么是单元测试
单元测试又称为模块测试,是指对软件中的最小可测试单元进行检查和验证。是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。
1、单元测试的本质
- 是一种验证行为
单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。
- 是一种设计行为
为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。
- 是程序优良的文档
从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。也就是说单元测试文件,他可以帮助我们更好的阅读和记录需求的功能点.
2、在什么时候我们需要单元测试
- 当发现自己改了代码之后,经常需要手动验证的时候
- 改了代码之后, 怕影响其他地方的bug
- 多人合作项目,可能交给其他人不放心别人改你的代码的时候
以上都可以通过写测试案例,进行自动化测试,从而减少bug。
3、单元测试的目的
-
保证代码的质量
代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。单元测试可以保证,我们的代码在不同的情况下代码都和你的期望一致
-
保证代码的可扩展性
为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行。
2、使用Xcode编写第一个测试案例。
1、 创建单元测试Target
2、 单元测试类介绍
如上图所示,其实Xcode已经说的很清楚了。
setUp
用于做一些初始化代码TearDown
用于对象的销毁- 所有测试类都必须继承
XCTestCase
- 测试方法必须以testXXX开头,Xcode会自动识别出所有的测试方法
- 在一个类中测试方法的调用顺序是按照方法的顺序来调用的
调用
- 执行所有测试方法:command + u
- 只执行某个测试方法:点击方法前的菱形(目前是对号/错号)
- 执行某个类的所有测试方法:点击类前的菱形(目前是对号/错号)
3、写案例测试
我们在viewController里面添加一个方法计算工资税。如图所示
我们想验证这个方法是否计算正确,便可以创建测试来测试。如下图所示
点每一个方法的左边的菱形图标就会单独测试。如果点类名旁边的菱形图标便会全类测试,执行方法按顺序执行。
如上图所示,第二第三个方法验证通过,第一个并没有通过。是我们通过XCTAssert
为我们提供的系统断言 XSTAsserTrue
进行的判断,那么除了 XSTAsserTrue
还有那些可以用于判断的呢。下面简单介绍常用的
4、常用断言
- (void)testExample {
NSLog(@"--- 失败之前的输出");
XCTFail(@"无论怎么样都是报错");
NSLog(@"--- 失败之后的输出");
}
- (void)testNil {
// XCTAssertNil(expression, ...)
// expression为空时通过,否则测试失败。
// expression接受id类型的参数。
// XCTAssertNotNil(expression, ...)
// expression不为空时通过,否则测试失败。
NSString *name = @"小明";
// XCTAssertNil(name, @"报错信息输出");
// XCTAssertNotNil(name, @"viewcontroller 是 nil ");
}
- (void)testTrue {
// XCTAssert(expression, ...)
// expression为true时通过,否则测试失败。
// expression接受boolean类型的参数。
// XCTAssertTrue(expression, ...)
// expression为true时通过,否则测试失败。
// expression接受boolean类型的参数。
// XCTAssertFalse(expression, ...)
// expression为false时通过,否则测试失败。
// expression接受boolean类型的参数。
NSInteger number = 10;
// XCTAssert(number == 10, @"报错信息输出");
// XCTAssertTrue(number == 10, @"报错信息输出");
XCTAssertFalse(number != 10, @"报错信息输出");
}
- (void)testEqual {
// XCTAssertEqualObjects(expression1, expression2, ...)
// 相当于 isEqual 先判断指针是否相等, 在判断是否是同类对象, 然后依次判断对应属性是否相等.
//
// XCTAssertNotEqualObjects(expression1, expression2, ...)
// XCTAssertEqual(expression1, expression2, ...)
// expression1和expression1相等时通过,否则测试失败。
// expression接受基本类型的参数(数值、结构体之类的)。
//
// XCTAssertNotEqual(expression1, expression2, ...)
// expression1和expression1不相等时通过,否则测试失败。
// expression接受基本类型的参数。
}
5、覆盖率
单元测试覆盖率可以直观的显示我们的单元测试是否充分,可以快速查找到我们的未覆盖到的类和方法.
Xcode为我们提供覆盖检测, 使用流程
Edit Scheme - Test - Options - 勾选 Gather coverage for (全部文件, 或者自定义部分文件)
如图所示
然后command+u 跑测试用例之后便可在如图所示,查阅单元测试的覆盖率
6、异步测试
XCTestExpectation 是系统为我们提供的异步测试的API,可以帮助我们测试接口,响应速度等。
测试方法,如下图
- 创建
XCTestExpectation
对象 - 设置等待时间
[self waitForExpectations:@[exp] timeout:0.5];
- 在计算完成时调用
fulfill
。 如果在规定时间内没有调用就算超时会报错。
7、简单介绍 Mock、Stub
- Mock 泛指模拟的类
- Stub 泛指模拟类的方法
当我们写单元测试的时候, 我们需要尽量避免实例化一些具体的组件来保持测试短又快, 保持单元测试的隔离. 在现在的项目中,测试组件之间依赖关系比较复杂, 我们可以使用mock对象来替代具体的依赖class, mock就是伪造的有预定义行为的对象替身, 被测试组件不知道其中的差异, 自定义数据, 然后有信心的测试. OCMock模拟类的三种方式
ViewController *vc = OCMPartialMock([[ViewController alloc] init]); // 先使用自己的, 在使用stub的
ViewController *vc = OCMStrictClassMock([ViewController class]); // 没有被stub的方法, 会抛出异常
ViewController *vc = OCMClassMock([ViewController class]); // 没有被stub的方法, 不会抛出异常
OCMStub模拟类的方法
OCMStub([studentTool testStudentMathResultLevel:[OCMArg any]]).andReturn(1);
在你使用studentTool的testStudentMathResultLevel方法时,无论你传入什么都会返回1
OCMStub([studentTool testStudentMathResultLevel:[OCMArg any]]).andCall(self, @selector(testStudentMathResultLevel:));
可以自己实现一个方法来替代原有的方法
同时我们在做OCMock
和OCStub
的时候有两点我们需要注意.
stub
一个mock
对象的方法后,不能在同一个mock
对象上再一次stub
这个方法,第二次的stub
无效。- 不可
stub
一个非mock
对象的方法,这种操作stub
是无效的。
8、耦合测试
在现实项目中我们需要测试的案例肯定比以上所列举内容要复杂。例如下举例说明:如图我们有一个计算 班级优秀数学成绩的人数方法如下。
这个方法又是依赖与另一个工具类如下。
如果我们直接测试 优秀英语学生数量的方法,是不行的。因为我们创建的测试对象 他并没有为 tool 初始化。如图所示:
- 直接创建一个
StudentResultTool
对象给viewController
即可 - 为
viewController
添加一个模拟的StudentResultTool
对象,如图 那这两种方法哪个更好呢 ? 有何不同吗?
不同之处在于第二种方式,我们可以修改
StudentResultTool
的testStudentMathResultLevel
的返回值, 直接返回我们想要的数据. 而第一种方法不行, 那这样又什么好处呢 ?
对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。
-
例如你可能要尝试100次才会返回一个NSError,通过mock object你可以自行创建一个NSError对象,测试在出错情况下程序的处理是否符合你的预期。
-
例如,当我们接口没有开发完成,我们又想使用接口数据进行下一步开发测试,这个时候我们可以使用mock,返回一些你指定的数据,从而绕过服务器。
-
例如假设你要访问一个数据库,但是访问过程的开销巨大,这时你可以虚拟一个数据库,并且返回一些自行定制的数据,从而绕过了数据库的访问。
mock的思想很简单:没有条件?我们就自行创造条件。
而真实的项目环境可能会比举例出来的情况更加复杂,例如
- 真实的对象是通过文件系统、数据库或者网络异步获取的
- 真实的对象运行效率低
- 真实的对象难以模拟,比如网络错误等
- 真实对象的行为有不确定性,无法通过真实对象覆盖全部场景
这些时候我们都可以通过使用mock对象来提高单元测试的效率
3、单元测试框架选择
1、行为驱动开发(BDD) 和 Kiwi框架 介绍
它通过用自然语言书写非程序员可读的测试用例扩展。最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。
Kiwi 如 BDD所说做到了 通过用自然语言书写非程序员可读的测试用例 例: 如图我们有一个Student类,并需要测试。
- 在所有开始之前我们需要一个stu对象
- 在所有之后我们需要销毁测试对象
- 这个学生对象应该有名字
- 这个学生对象应该有所有科目有分数
- 他应该有总分,并且计算正确
这样一个流程,就是一个完成的测试流程。在测试过程中如果哪个环节出错了,一目了然。
2、测试驱动开发(TDD) XCTest+OCMock
测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。
因为XCTest
与Xcode
深度集成,这也是很多人选择TDD
,避免第三方的集成。在有需要的情况下在集成OCMock
。
在第三章的Mock, Stub介绍都是使用OCMock来完成的,如下
我们做的操作基本可以列成三部
- Mock一个
Student
对象 - Mock一个
StudentTool
对象,并验证 - 帮Mock的
StudentTool
置换给Student
的tool
并验证
由上我们可以看出一个问题,有依赖的方法是不方便与测试的。所以说单元测试也是一种设计行为,为了可以让代码得到优质的测试环境,我们就会去写一个优质,低耦合,逻辑清晰的代码。这也是
TDD
的意义所在。
3、方案对比
由上文所说,似乎各有优点,使用类似Kiwi
框架的BDD
一个是以讲故事的形式来写测试用例。使用XCTest + OCMock
的TDD
可以让程序员写出更好的代码。那么他们是否有自的缺点呢。如下图所示:
TDD
更占优势。在我们不集成第三方的情况XCTest
也能供我们编写测试案例。
并且当你尝试去写Kiwi
的时候,你会发现Kiwi
在未完全执行所有测试用例时,是无法看到单个测试方法的,更无法执行单个测试。Kiwi
的最小测试单位为一个测试用例类,而XCTest
的最小测试单位为测试用例类的一个测试方法。
那么既然XCTest+OCMock
这么好,别的第三方在写测试用例的时候也都是这么选择的吗 ? 如图
- 我们看一下AFNetWorking的单元测试, 这里只截取比较常用的GET请求和POST请求单元测试.
-
setUp
创建sessionManager
并在tearDown
里面销毁 -
GET
请求测试, 如下图, 创建期望, 在一定的期望之内完成请求,并返回值不为空. -
POST
请求测试, 逻辑和上面是一样的. 他这里的waitForExpectationsWithCommonTimeout
是AF自己封装的超时等待, 默认是20秒.
想看更多关于AF的单元测试可以自行下载查阅.
- 还有如图截选自FMDB单元测试:
总结我认为 XCTest+OCMock是更好的选择。
参考文献: