forked from 100mango/zen
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# iOS单元测试入门指南 | ||
|
||
这篇文章主要帮助大家了解什么是单元测试,为什么要编写单元测试,如何编写单元测试,如何写出更好的单元测试。 | ||
|
||
## What | ||
|
||
单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。在 Objectice-C,Swift 中通常指对一个类,一个方法进行测试。 | ||
|
||
## Why | ||
|
||
- 验证代码的正确性 | ||
|
||
我们编写的代码基本上不能像数学一样通过形式证明(Formal proof)来保证正确性。但是我们能借助单元测试来保证代码的正确性。单元测试将我们的模块和代码分割成一个个的测试案例,我们通过无法发现错误来证明我们代码的正确性。正如Bob大叔在他的《Clean Architecture》一书所说的:“software is like a science. We show correctness by failing to prove incorrectness” | ||
|
||
- 更好的代码 | ||
|
||
我们希望借助单元测试来帮助我们设计良好可读的接口,写出高内聚,低耦合的代码。 | ||
当我们写完一个模块,经常会被其他人调用的接口时。就可以借助单元测试来验证我们代码是否如期运行。如果发现自己的接口和功能很难通过单元测试去验证,我们就需要审视我们的接口写得好不好,代码是否有提升的空间。 | ||
|
||
- 更好的文档 | ||
|
||
一个好的单元测试就是一份优秀的文档。使用者只需看单元测试的代码,就能够明白如何使用这个模块,这个API。 | ||
|
||
- 更好的Debug | ||
|
||
单元测试能够帮助我们快速定位到bug,因为单元测试颗粒度小,我们能具体定位到是哪个函数,哪个测试案例出现了问题。如果当前的单元测试不能发现问题,我们可以写新的单元测试来验证问题,修复以后,以后就能每次对问题进行验证,防止问题重现。 | ||
|
||
- 更好的重构 | ||
|
||
单元测试能够保证代码在修改后,仍然保持预期的功能,让重构和修改代码变得更稳健。 | ||
|
||
|
||
## How | ||
|
||
在Xcode中进行单元测试是一件十分容易的事情,我们通过XCTest框架来进行单元测试。 | ||
|
||
1. 添加一个新的测试Target | ||
|
||
![](test.gif) | ||
|
||
|
||
2. 编写单元测试 | ||
|
||
~~~objective-c | ||
@interface WeChatTests : XCTestCase | ||
@end | ||
|
||
@implementation WeChatTests | ||
|
||
- (void)testOnePlusOne { | ||
//Given | ||
NSInteger a = 1; | ||
NSInteger b = 1; | ||
//When | ||
NSInteger result = a + b; | ||
//then | ||
XCTAssertTrue(result == 2); | ||
} | ||
@end | ||
~~~ | ||
|
||
可以看到编写单元测试的步骤是非常简单的。 | ||
|
||
- 我们的单元测试是在`XCTestCase`的子类中进行 | ||
- 每个以 test 为开头,无参数,无返回值的方法都是一个测试用例。 | ||
- 我们通过断言来判断测试结果是否正确。 如上面的`XCTAssertTrue`。 | ||
|
||
XCTest框架的详细使用教程,请查看:[单元测试简明指南](xctestcookbook.markdown) | ||
|
||
|
||
## Guideline | ||
|
||
我们知道如何编写单元测试后,更重要的是知道如何编写更好的单元测试。 | ||
|
||
### 代码组织 | ||
|
||
我们可以借助`BDD(行为驱动开发)`的理念来组织代码和思路: | ||
|
||
`Given-When-Then` 模式: | ||
|
||
以我们上面的简单测试为例: | ||
|
||
~~~objective-c | ||
- (void)testOnePlusOne { | ||
//Given | ||
NSInteger a = 1; | ||
NSInteger b = 1; | ||
//When | ||
NSInteger result = a + b; | ||
//then | ||
XCTAssertTrue(result == 2); | ||
} | ||
~~~ | ||
|
||
可以看到代码是以这样的顺序来组织的: | ||
|
||
- Given:测试所需要的环境,相当于一个前置条件。 | ||
|
||
- When:触发被测事件,类似上面的算术运算。 | ||
|
||
- Then:验证结果,在这里就是我们的断言。 | ||
|
||
|
||
### 如何编写出优秀的单元测试 | ||
|
||
**FIRST** 原则 | ||
|
||
- Fast: 每个单元测试需要足够快,为了达到这个目的,我们的测试代码需要足够的精简。 | ||
|
||
- Isolated/Independent: 测试之间是独立的,一个测试不依赖另外一个测试,不依赖外部环境。 | ||
|
||
- Repeatable: 同一个测试,每次的测试结果是相同的。 | ||
|
||
- Self-validating:不需要人工检查来判断测试是正确还是错误的。在XCTest框架中,意味着我们通过断言来判断测试是否正确,不需要靠看日志等人工检查的方式。 | ||
|
||
- Thorough and Timely: 覆盖完整的的代码路径,包括错误的情况。理想的情况下,编写代码前,就先准备好单元测试(TDD)。 | ||
|
||
其他指南 | ||
|
||
- 单元测试需要由最熟悉代码的人(代码的编写者)来写。 | ||
|
||
我们不能把测试的责任推脱给测试人员,作为代码的编写者,我们更能把握好代码的细节,边界情况。 | ||
|
||
- 单元测试应该集成到自动测试的框架中。 | ||
|
||
以XCTest框架为例,我们可以把我们的测试继承到Jenkins中,每天定时运行,自动和及时发现问题。 | ||
|
||
### 何时需要单元测试 | ||
|
||
理想的情况下,我们所有的代码都有对应的单元测试。这是不现实的,但是有些单元测试的性价比是很高的,值得我们去做。 | ||
|
||
1. 频繁被调用或频繁变更的代码 | ||
|
||
- 越是频繁被其他模块使用的代码,就越值得测试。像是最近我编写的XML和JSON的序列化和反序列化统一解决方案,就值得去做单元测测。这能保证我们底层模块,基础模块的稳定性和健壮性。 | ||
- 越是频繁变更的代码,就越值得测试。这能够保证我们在修改代码时的稳定性和健壮性。 | ||
|
||
2. Debug | ||
|
||
- 在开发的过程中,单元测试可以用来测试易错的地方和边界情况。 | ||
|
||
- 在维护的过程中,单元测试用来验证bug, 一旦修复后,我们可以确保这个Bug是修复了,并且以后也能一直被追踪。 | ||
|
||
|
||
## 总结 | ||
|
||
测试是工具而不是目的。我们希望通过测试来使得代码变得更健壮,更优雅,让Debug和重构变得更简单。 | ||
|
||
#### 参考资料 | ||
|
||
- Robert C. Martin. 《Clean Architecture:》 | ||
- [XCTest 测试实战](https://objccn.io/issue-15-2/) | ||
- [INTRODUCING BDD](https://dannorth.net/introducing-bdd/) | ||
- [Testing with Xcode](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html) | ||
- [单元测试要做多细?](https://coolshell.cn/articles/8209.html) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# 单元测试简明指南 | ||
|
||
## 创建测试 | ||
|
||
![](test.gif) | ||
|
||
|
||
## 运行测试 | ||
|
||
点击运行测试的按钮,或快捷键cmd + U | ||
|
||
![](runtest.png) | ||
|
||
|
||
## 编写逻辑测试 | ||
|
||
~~~objective-c | ||
@interface WeChatTests : XCTestCase | ||
@end | ||
|
||
@implementation WeChatTests | ||
|
||
- (void)testOnePlusOne { | ||
//Given | ||
NSInteger a = 1; | ||
NSInteger b = 1; | ||
//When | ||
NSInteger result = a + b; | ||
//then | ||
XCTAssertTrue(result == 2); | ||
} | ||
@end | ||
~~~ | ||
- 我们的单元测试是在`XCTestCase`的子类中进行 | ||
- 每个以 test 为开头,无参数,无返回值的方法都是一个测试用例。 | ||
- 我们通过断言来判断测试结果是否是正确的。 如上面的`XCTAssertTrue`。 | ||
常用的断言有: | ||
~~~objective-c | ||
//直接输出错误 | ||
XCTFail(...) | ||
//expression为空时通过 | ||
XCTAssertNil(expression, ...) | ||
//expresion不为空时通过 | ||
XCTAssertNotNil(expression, ...) | ||
//expression为true时通过 | ||
XCTAssert(expression, ...) | ||
//expression为true时通过 | ||
XCTAssertTrue(expression, ...) | ||
//expression为false时通过 | ||
XCTAssertFalse(expression, ...) | ||
//expression1和expression1地址相同时通过 | ||
XCTAssertEqualObjects(expression1, expression2, ...) | ||
//expression1和expression1地址不相同时通过 | ||
XCTAssertNotEqualObjects(expression1, expression2, ...) | ||
//expression1和expression1相等时通过 | ||
XCTAssertEqual(expression1, expression2, ...) | ||
//expression1和expression1不相等时通过 | ||
XCTAssertNotEqual(expression1, expression2, ...) | ||
~~~ | ||
|
||
## 编写异步测试 | ||
|
||
~~~objective-c | ||
- (void) testAsyncFunction { | ||
XCTestExpectation *exp = [self expectationWithDescription:@"异步操作出错"]; | ||
|
||
//Given | ||
NSOperationQueue *queue = [[NSOperationQueue alloc]init]; | ||
[queue addOperationWithBlock:^{ | ||
//When | ||
sleep(2); | ||
//Then | ||
XCTAssertEqual(@"a", @"a"); | ||
//如果断言没问题,就调用fulfill宣布测试完成 | ||
[exp fulfill]; | ||
}]; | ||
|
||
//设置延迟三秒后,如果还没调用fulfill,则报错 | ||
[self waitForExpectationsWithTimeout:3 handler:nil]; | ||
} | ||
~~~ | ||
|
||
异步测试的步骤也是很简单的: | ||
|
||
1. 定义一个或者多个`XCTestExpectation`,代表我们想要得到的结果 | ||
2. 编写异步代码,断言,在异步代码的最后加上`fulfill`宣布测试完成 | ||
3. 调用`waitForExpectationsWithTimeout`设置延迟的时间,超过这段时间,异步测试就会判断为失败 | ||
|
||
|
||
|
||
## 编写性能测试 | ||
|
||
![](measure.png) | ||
|
||
性能测试也是很简单的,调用`measureBlock`这个接口,将需要测试的代码作为block传递进去即可。XCTest会跑十遍代码。 | ||
|
||
console会输出执行的平均时间: | ||
|
||
~~~python | ||
Test Case '-[WeChatTests testPerformanceExample]' | ||
measured [Time, seconds] average: 2.000, | ||
relative standard deviation: 0.018%, | ||
values: [2.001027, 2.000147, 2.000313, 2.000506, 2.000062, 2.000056, 2.000268, 2.000176, 2.000287, 2.001102] | ||
~~~ | ||
|
||
我们可以修改`Baseline`来确定最长可执行的时间,修改`Max STDDEV`来确定上下浮动的范围。比如上图,我们修改`Baseline`为1s,而我们sleep了两秒,那么这个测试就失败了。 | ||
|
||
![](fail.png) | ||
|
||
|
||
#### 参考资料 | ||
|
||
[苹果Testing文档](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.