docs: Charles 高级技巧

This commit is contained in:
binpeng.liu
2022-11-29 23:56:21 +08:00
parent f0e20eaf2e
commit 09cb172354
8 changed files with 187 additions and 122 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -208,7 +208,7 @@ mov word ptr [0], 66h
栈是一种后进先出特点的数据存储空间LIFO
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Stack.png)
![](./../assets//Stack.png)
- 8086 会将 CS 作为代码段的段地址,将 `CS:IP` 指向的指令作为下一条需要取出执行的指令
@@ -224,7 +224,7 @@ SP堆栈寄存器存放栈的偏移地址
#### push
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StackPush.png)
![](./../assets//StackPush.png)
`push ax` 指令执行,会拆解为:
@@ -236,7 +236,7 @@ ax = ah + al所以 ax 中的数据入栈需要占据2个单位sp = sp - 2
#### pop
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/stackPop.png)
![](./../assets//stackPop.png)
`pop ax` 指令执行,会拆解为:
@@ -248,7 +248,7 @@ ax = ah + al所以 ax 中的数据入栈需要占据2个单位sp = sp - 2
当一个栈空间是空的时候,`SS:SP` 指向栈空间最高地址单元的下一个单元。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/emptyStack.png)
![](./../assets//emptyStack.png)
当一个栈空或者满的时候,执行 PUSH、POP 指令需要注意,因为 `SP = SP + 2``SP = SP - 2` 都会导致将错误的数据入栈或者错误的数据出栈,导致发生不可预期的事情。
@@ -457,7 +457,7 @@ int main(int argc, const char * argv[]) {
}
```
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleReturn.png)
![](./../assets//AssembleReturn.png)
可以看到 return 的值是保存在 eax 寄存器中。为什么是 ee是32位的意思环境老款 MBP 电脑运行)。
@@ -571,7 +571,7 @@ int main(int argc, const char * argv[]) {
CPU 会在栈内部将局部变量的地方临时分配10字节大小空间用来存储局部变量。这个怎么实现呢`SP = SP - 10` 这条指令用来将栈顶指针改变留出10字节大小空间。但是留出的空间是空的万一 `CS:IP` 指向这块区域会把里面的数据当作指令去执行则可能发生一些不可预知的错误。Windows 平台,针对预留的局部变量空间,会走动填充 cc也就是 `int 3 ` 断点中断,只要 `CS:IP` 去执行就会断点中断,更安全。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleFunctionStack.png)
![](./../assets//AssembleFunctionStack.png)
关键代码如下:
@@ -635,6 +635,6 @@ sum:
Stack Frame Layout代表一个函数的执行环境。包括参数、返回地址、局部变量和包括在本函数内部执行的所有内存操作等
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StackFrame.png)
![](./../assets//StackFrame.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSStackFrame.png)
![](./../assets//CSStackFrame.png)

View File

@@ -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 是需要单独可运行的)
![](./../assets/UnitTest-FunctionStandard.png)
再有一个参照物,大家都在用的 ITerm2 的源码是开源的,看看 ITerm2 是如何写测试的
![](./../assets/ITerm2-TestCase.png)
### 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
@interface HermesClient : NSObject
@end
@interface HermesClient ()
@property (nonatomic, assign) int b;
@end
@implementation HermesClient
- (instancetype)init {
if (self = [super init]) {
self.b = 10;
}
return self;
}
- (int)add:(int)a {
return a+b;
}
@end
// HermesClientTest.m
@interface HermesClient (UnitTest)
@property (nonatomic, assign) int b;
- (int)add:(int)a;
@end
- (NSString *)name;
- (void)hello;
@interface HermesClientTest() {
HermesClientTest *_sut;
}
@end
@implementation HermesClientTest
- (void)testPrivatePropertyAndMethod
- (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>![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-07-13-TDDStructure.png)
<a name="TDDStructure"></a>![](./../assets/2020-07-13-TDDStructure.png)
- 先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态
- 然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态
@@ -233,7 +281,7 @@ TDD 开发过程类似下图:
1. 新建一个工程,确保 “Include Unit Tests” 选项是选中的状态
![TDD Step 1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-07-13-TDDStep1.png)
![TDD Step 1](./../assets/2020-07-13-TDDStep1.png)
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 数据
生成 Crash 类型的 meta 数据
@return meta 类型的字典
*/
- (NSDictionary *)generateCrashMetaDataFromReport;
@return meta 类型的字典
*/
@end
- (NSDictionary *)generateCrashMetaDataFromReport;
NS_ASSUME_NONNULL_END
@end
// HCTTestCase.m
#import "HCTTestCase.h"
#import ...
NS_ASSUME_NONNULL_END
@implementation HCTTestCase
// HCTTestCase.m
#import "HCTTestCase.h"
#import ...
#pragma mark - life cycle
@implementation HCTTestCase
- (void)setUp
{
[super setUp];
self.networkTimeout = 20.0;
// 1. 设置平台信息
[self setupAppProfile];
// 2. 设置 Mget 配置
[[TITrinityInitManager sharedInstance] setup];
// ....
// 3. 设置 HermesClient
[[HermesClient sharedInstance] setup];
}
#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];
}
- (void)tearDown
{
[super tearDown];
}
#pragma mark - public Method
- (void)waitForExpectationsWithCommonTimeout
{
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}
- (void)waitForExpectationsWithCommonTimeout
{
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
{
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
}
- (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];
}
- (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 的断言 XCAssertEx
- 反向匹配,很灵活。断言匹配用 `except(...).to.equal(...)`,断言不匹配则使用 `.notTo` 或者 `.toNot`
- 延时匹配,可以在链式表达式后加入 `.will`、`.willNot`、`.after(interval)` 等
#### 3. OCMock
### 4. 小结
Xcode 自带的 `XCTestCase` 比较适合 TDD不影响源代码系统独立且不影响 App 包大小。适合简单场景下的测试。且每个函数在最左侧又个测试按钮,点击后可以单独测试某个函数。
@@ -1064,12 +1110,8 @@ UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做
如果是一些活动页和逻辑经常变动的,老老实实走测试黑盒...
我觉得一直有个误区,就是觉得自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的
我觉得大家一直有个误区,就是觉得软件测试是为了质量额外做的功,这部分功是有用功还是无用功是不一定的。其实,有了正确的开发姿势后(那什么叫正确的开发姿势?设计一个函数的时候就应该想好该方法如何更方便的被测试),最后可以可以实现事半功倍的效果,良好的质量都是附送的,也就是说测试先行是让开发更快更好的展开。
![测试占比](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-07-14-TestingPercentage.png)
![测试占比](./../assets/2020-07-14-TestingPercentage.png)
WWDC 这张图也很清楚UI 其实需要的占比较小,还是要靠单测驱动。
## 参考资料
- [维基百科:测试驱动开发](https://zh.wikipedia.org/wiki/测试驱动开发)

View File

@@ -13,6 +13,7 @@
- 服务器压力测试
- 反向代理
- 解决与翻墙软件的冲突
- 电脑已经可以科学上网了,手机如何科学上网?
@@ -134,7 +135,6 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所
- iPhone 打开设置 -> 通用 -> 关于本机 -> 证书信任设置 -> 开启开关
![手机端CA证书的信任](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9a9ca26c907.png)
- 在 Charles 菜单栏 Proxy -> SSL Proxying Setting -> 点击 Add 按钮 -> 在弹出的对对话框设置需要监听的 HTTPS 域(*:代表通配符)
![HTTPS抓包端口和主机设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9aaad2c0ff8.png)
@@ -265,13 +265,36 @@ Charles 的反向代理功能允许我们将本地指定端口的请求映射到
Charles 的工作原理是把自己设置为系统的代理服务器,但是我们开发者经常会利用 VPN 翻墙访问谷歌查找资料这些翻墙软件的工作原理也是把自己设置成为系统的代理服务器为了2者和平共处。我们可以在 Charles 的 External Proxy Settings 中将翻墙的代理端口等信息填写。同时我们需要关闭翻墙软件的自动设置,更改为**“手动模式”**。(使其不主动修改系统代理)
## 电脑已经可以科学上网了,手机如何科学上网?
我们有时候会面临这样的问题,我们的电脑有科学上网能力,手机没有。这时候可以利用 Charles 强大的 **External Proxy Setting** 能力。
前提条件是:电脑具备科学上网能力、手机通过 Charles 设置代理到电脑上。
具体操作路径:顶部菜单栏 -> Proxy -> External Proxy Settings点击勾选后弹出设置面板如下图
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assetsCharles-ExternalProxySettings.png)
查看你电脑上科学上网工具的代理端口。我使用的是 clashX打开菜单栏 -> Help -> Ports 可以查看端口信息
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assetsClashx-Ports.png)
然后:
- 将顶部的 Use external proxy servers checkbox 进行勾选
- 将 Web ProxyHTTP进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890
- 将 Secure Web Proxy进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890
- 将 SOCKS Proxy 进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890
这样我们的手机也具备科学上网能力啦。
TipsiPhone 的 App Store 内置了苹果证书Certificate pinning所以抓包也就是伪造证书的方式是不行的。
## 总结
Charles 功能强大、界面简洁,读完这篇文章并做出练习,相信你能很快掌握它,“工欲善其事,必先利其器” ,掌握了它,相信可以为你大大提高开发中调试网络的效率。**Enjoy yourself**
## 参考链接
[唐巧的博客](http://blog.devtang.com/2015/11/14/charles-introduction/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/Clashx-Ports.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
assets/Iterm2-TestCase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB