mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
docs: Charles 高级技巧
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
|
||||
测试很重要!测试很重要!测试很重要!重要的事情说三遍。
|
||||
|
||||
场景1:每次我们写完代码后都需要编译运行,以查看应用程序的表现是否符合预期。假如改动点、代码量小,那验证成本低一些,假如不符合预期,则说明我们的代码有问,人工去排查问题花费的时间也少一些。假如改动点很多、受影响的地方较多,我们首先要大概猜测受影响的功能,然后去定位问题、排查问题的成本就很高。
|
||||
场景1:每次我们写完代码后都需要编译运行,以查看应用程序的表现是否符合预期。假如改动点、代码量小,那验证成本低一些,假如不符合预期,则说明我们的代码有问,人工去排查问题花费的时间也少一些。假如改动点很多、受影响的地方较多,我们首先要梳理相关影响点,然后去定位问题、排查问题的成本就很高。
|
||||
|
||||
场景2:你新接手的 SDK 某个子功能需要做一次技术重构。但是你只有在公司内部的代码托管平台上可以看到一些 Readme、接入文档、系统设计文档、技术方案评估文档等一堆文档。可能你会看完再去动手重构。当你重构完了,找了公司某条业务线的 App 接入测试,点了几下发现发生了奔溃。😂 心想,本地测试、debug 都正常可是为什么接入后就 Crash 了。其实想想也好理解,你本地重构只是确保了你开发的那个功能运行正常,你很难确保你写的代码没有影响其他类、其他功能。假如之前的 SDK 针对每个类都有单元测试代码,那你在新功能开发完毕后完整跑一次单元测试代码就好了,保证每个 Unit Test 都通过、分支覆盖率达到约定的线,那么基本上是没问题的。
|
||||
场景2:你新接手的 SDK 某个子功能需要做一次技术重构。但是你只有在公司内部的代码托管平台上可以看到一些 Readme、接入文档、系统设计文档、技术方案评估文档等一堆文档。可能你会看完再去动手重构。当你重构完了,找了公司某条业务线的 App 接入测试,点了几下发现发生了奔溃。😂 心想,本地测试、debug 都正常可是为什么接入后就 Crash 了。其实想想也好理解,你本地重构只是确保了你开发的那个功能运行正常,你很难确保你写的代码没有影响其他类、其他功能。假如之前的 SDK 针对每个类都有单元测试代码,那你在新功能开发完毕后完整跑一次单元测试代码就好了,保证每个 Unit Test 都通过、分支覆盖率达到约定的值,那基本上是没问题的,或者说说问题的概率非常低了。
|
||||
|
||||
场景3:在版本迭代的时候,计划功能 A,从开发、联调、测试、上线共2周时间。老司机做事很自信,这么简单的 UI、动画、交互,代码风骚,参考服务端的「领域驱动」在该 feature 开发阶段落地试验了下。联调、本地测试都通过了,还剩3天时间,本以为测试1天,bug fix 一天,最后一天提交审核。代码跟你开了个玩笑,测试完 n 个 bug(大大超出预期)。为了不影响 App 的发布上架,不得不熬夜修 bug。将所有的测试都通过测试工程师去处理,这个阶段理论上质量应该很稳定,不然该阶段发现代码异常、技术设计有漏洞就来不及了,你需要协调各个团队的资源(可能接口要改动、产品侧要改动),这个阶段造成改动的成本非常大。
|
||||
|
||||
@@ -118,6 +118,18 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
}
|
||||
```
|
||||
|
||||
Bad Case:
|
||||
|
||||
```objective-c
|
||||
- (void)testShouldIEatSomething
|
||||
{
|
||||
// case1
|
||||
// case2
|
||||
}
|
||||
```
|
||||
|
||||
Good Case:
|
||||
|
||||
```objective-c
|
||||
- (void)testShouldIEatSomethingWhenHungry
|
||||
{
|
||||
@@ -130,6 +142,16 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
}
|
||||
```
|
||||
|
||||
QA:一个被测方法,有诸多 case,为什么不写在一个测试方法中将这些 case 写全?
|
||||
|
||||
因为测试有个原则,就是每个测试 case 都是能够单独可运行的。如果2个case都在一个方法内,那就不是单独可运行。怎么理解,贴个图(点击最左边的小菱形,该 case 是需要单独可运行的)
|
||||
|
||||

|
||||
|
||||
再有一个参照物,大家都在用的 ITerm2 的源码是开源的,看看 ITerm2 是如何写测试的
|
||||
|
||||

|
||||
|
||||
### 3. 明确标识被测试类
|
||||
|
||||
这条主要站在团队合作和代码可读性角度出发来说明。写过单元测试的人都知道,可能某个函数本来就10行代码,可是为了测试它,测试代码写了30行。一个方法这样写问题不大,多看看就看明白是在测试哪个类的哪个方法。可是当这个类本身就很大,测试代码很大的情况下,不管是作者自身还是多年后负责维护的其他同事,看这个代码阅读成本会很大,需要先看测试文件名 `代码类名 + Test` 才知道是测试的是哪个类,看测试方法名 `test + 方法名` 才知道是测试的是哪个方法。
|
||||
@@ -178,33 +200,59 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
|
||||
### 4. 使用分类来暴露私有方法、私有变量
|
||||
|
||||
某些场景下写的测试方法内部可能需要调用被测对象的私有方法,也可能需要访问被测对象的某个私有属性。但是测试类里面是访问不到被测类的私有属性和私有方法的,借助于 `Category` 可以实现这样的需求。
|
||||
某些场景下写的测试方法内部可能需要调用被测对象的私有方法,也可能需要访问被测对象的某个私有属性。但是测试类里面是访问不到被测类的私有属性和私有方法的,借助于 Runtime 特性,我们可以给被测类添加 **Category** 来解决无法访问私有方法和私有属性的问题。
|
||||
|
||||
为测试类添加一个分类,后缀名为 `UnitTest`。如下所示
|
||||
|
||||
`HermesClient` 类有私有属性 `@property (nonatomic, strong) NSString *name;`,私有方法 `- (void)hello`。为了在测试用例中访问私有属性和私有方法,写了如下分类
|
||||
|
||||
```objective-c
|
||||
// HermesClientTest.m
|
||||
|
||||
@interface HermesClient (UnitTest)
|
||||
|
||||
- (NSString *)name;
|
||||
|
||||
- (void)hello;
|
||||
|
||||
@interface HermesClient : NSObject
|
||||
@end
|
||||
|
||||
@interface HermesClient ()
|
||||
@property (nonatomic, assign) int b;
|
||||
@end
|
||||
|
||||
@implementation HermesClientTest
|
||||
@implementation HermesClient
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
self.b = 10;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)testPrivatePropertyAndMethod
|
||||
- (int)add:(int)a {
|
||||
return a+b;
|
||||
}
|
||||
@end
|
||||
|
||||
// HermesClientTest.m
|
||||
@interface HermesClient (UnitTest)
|
||||
@property (nonatomic, assign) int b;
|
||||
- (int)add:(int)a;
|
||||
@end
|
||||
|
||||
@interface HermesClientTest() {
|
||||
HermesClientTest *_sut;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation HermesClientTest
|
||||
- (void)setUp {
|
||||
_sut = [[HermesClientTest alloc] init];
|
||||
}
|
||||
- (void)testAdd
|
||||
{
|
||||
NSLog(@"%@",[HermesClient sharedInstance].name);
|
||||
[[HermesClient sharedInstance] hello];
|
||||
int result = [_sut add:20];
|
||||
XCTAssert(result == 30,
|
||||
@"Oops, there is something wrong with add: method of HermesClientTest.");
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 四、 单元测试下开发模式、技术框架选择
|
||||
|
||||
单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的 TDD、BDD 方案。
|
||||
@@ -219,7 +267,7 @@ TDD 强调不断的测试推动代码的开发,这样`简化了`代码,保
|
||||
|
||||
TDD 开发过程类似下图:
|
||||
|
||||
<a name="TDDStructure"></a>
|
||||
<a name="TDDStructure"></a>
|
||||
|
||||
- 先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态
|
||||
- 然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态
|
||||
@@ -233,7 +281,7 @@ TDD 开发过程类似下图:
|
||||
|
||||
1. 新建一个工程,确保 “Include Unit Tests” 选项是选中的状态
|
||||
|
||||

|
||||

|
||||
|
||||
2. 创建后的工程目录如下
|
||||
|
||||
@@ -329,6 +377,10 @@ TDD 开发过程类似下图:
|
||||
|
||||
### 3. XCTest
|
||||
|
||||
看完了 TDD 和 BDD,那我们选什么?
|
||||
|
||||
|
||||
|
||||
**开发步骤**
|
||||
|
||||
Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
@@ -581,94 +633,82 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
@interface HCTTestCase : XCTestCase
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval networkTimeout;
|
||||
```
|
||||
|
||||
/// 用一个默认时间设置异步测试 XCTestExpectation 的超时处理
|
||||
- (void)waitForExpectationsWithCommonTimeout;
|
||||
|
||||
/**
|
||||
用一个默认时间设置异步测试 XCTestExpectation 的超时处理
|
||||
*/
|
||||
- (void)waitForExpectationsWithCommonTimeout;
|
||||
|
||||
/**
|
||||
用一个默认时间设置异步测试的
|
||||
|
||||
@param handler 超时的处理逻辑
|
||||
*/
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
|
||||
|
||||
用一个默认时间设置异步测试的
|
||||
@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];
|
||||
}
|
||||
|
||||
生成 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];
|
||||
}
|
||||
|
||||
- (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
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
- (void)setupAppProfile
|
||||
{
|
||||
[[CMAppProfile sharedInstance] setMPlatform:@"70"];
|
||||
// ...
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
2. 上述说的基本是开发规范相关。测试方法内部如果调用了其他类的方法,则在测试方法内部必须 Mock 一个外部对象,限制好返回值等。
|
||||
|
||||
3. 在 XCTest 内难以使用 mock 或 stub,这些是测试中非常常见且重要的功能
|
||||
@@ -821,6 +861,12 @@ Expecta 是一个匹配(断言)框架,相比 Xcode 的断言 XCAssert,Ex
|
||||
- 反向匹配,很灵活。断言匹配用 `except(...).to.equal(...)`,断言不匹配则使用 `.notTo` 或者 `.toNot`
|
||||
- 延时匹配,可以在链式表达式后加入 `.will`、`.willNot`、`.after(interval)` 等
|
||||
|
||||
#### 3. OCMock
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 4. 小结
|
||||
|
||||
Xcode 自带的 `XCTestCase` 比较适合 TDD,不影响源代码,系统独立且不影响 App 包大小。适合简单场景下的测试。且每个函数在最左侧又个测试按钮,点击后可以单独测试某个函数。
|
||||
@@ -1064,12 +1110,8 @@ UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做
|
||||
|
||||
如果是一些活动页和逻辑经常变动的,老老实实走测试黑盒...
|
||||
|
||||
我觉得一直有个误区,就是觉得自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的
|
||||
我觉得大家一直有个误区,就是觉得软件测试是为了质量额外做的功,这部分功是有用功还是无用功是不一定的。其实,有了正确的开发姿势后(那什么叫正确的开发姿势?设计一个函数的时候就应该想好该方法如何更方便的被测试),最后可以可以实现事半功倍的效果,良好的质量都是附送的,也就是说测试先行是让开发更快更好的展开。
|
||||
|
||||

|
||||

|
||||
|
||||
WWDC 这张图也很清楚,UI 其实需要的占比较小,还是要靠单测驱动。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [维基百科:测试驱动开发](https://zh.wikipedia.org/wiki/测试驱动开发)
|
||||
|
||||
Reference in New Issue
Block a user