diff --git a/.DS_Store b/.DS_Store index 1cfd3f9..e080bde 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Chapter1 - iOS/1.109.md b/Chapter1 - iOS/1.109.md index af858fb..70afc7f 100644 --- a/Chapter1 - iOS/1.109.md +++ b/Chapter1 - iOS/1.109.md @@ -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 寄存器中。为什么是 e,e是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) \ No newline at end of file +![](./../assets//CSStackFrame.png) \ No newline at end of file diff --git a/Chapter1 - iOS/1.75.md b/Chapter1 - iOS/1.75.md index f208e01..0207338 100644 --- a/Chapter1 - iOS/1.75.md +++ b/Chapter1 - iOS/1.75.md @@ -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 -// 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 开发过程类似下图: -![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-07-13-TDDStructure.png) +![](./../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 数据 - - @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 还是建议在核心逻辑且长时间没有改动的情况下去做 如果是一些活动页和逻辑经常变动的,老老实实走测试黑盒... -我觉得一直有个误区,就是觉得自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的 +我觉得大家一直有个误区,就是觉得软件测试是为了质量额外做的功,这部分功是有用功还是无用功是不一定的。其实,有了正确的开发姿势后(那什么叫正确的开发姿势?设计一个函数的时候就应该想好该方法如何更方便的被测试),最后可以可以实现事半功倍的效果,良好的质量都是附送的,也就是说测试先行是让开发更快更好的展开。 -![测试占比](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/测试驱动开发) diff --git a/Chapter7 - Geek Talk/7.2.md b/Chapter7 - Geek Talk/7.2.md index 71d25df..1eb170c 100644 --- a/Chapter7 - Geek Talk/7.2.md +++ b/Chapter7 - Geek Talk/7.2.md @@ -13,6 +13,7 @@ - 服务器压力测试 - 反向代理 - 解决与翻墙软件的冲突 +- 电脑已经可以科学上网了,手机如何科学上网? @@ -75,7 +76,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 - 方法3:一般打开 Charles 并设置好配置信息后(比如电脑本机或者设置过代理的手机)所有的网络请求都将在 Charles 的面板上显示,同时我们感兴趣的网络请求如果也在面板上显示的话,**“Structure”模式下**可以选中需要分析的网络请求,鼠标右击选择**“Focus”**。**“Sequence”模式下**可以在面板的网络请求显示面板的右下角看到一个**Focus**按钮,点击勾选后 Charles 只会显示你感兴趣的网络请求。 ![Structure模式下Focus过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午9.22.39.png) - + ![Sequence模式下Focus过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-092259.png) @@ -119,7 +120,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 ![Charles确认开启抓取HTTPS](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午9.47.09.png) - + 如果你需要捕获移动设备的 HTTPS 网络请求,则需要在移动设备上安装证书并作简单的设置 - 选择 Charles 顶部菜单栏选择 **“Help” ->"Install Charles Root Certificate on a Mobile Device or Remote Browser"**。然后就可以看到 Charles 弹出的安装说明了。 @@ -134,13 +135,12 @@ 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) - 设置完毕,尽情抓取你想要的 HTTPS 网络请求吧。 ![抓取京东HTTPS数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9a9a966fafe.png) - + @@ -149,9 +149,9 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 在平时开发的时候我们经常需要模拟弱网环境,并作弱网环境下的适配工作。Charles 为我们提供了这个服务。 在 Charles 菜单栏选择 **“Proxy” -> "Throttle Settings"**。在弹出的面板上设置网络请求的参数(上行,下行带宽、利用率、可靠性等等信息)。如下图所示。 - + ![模拟弱网环境](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午10.27.22.png) - + 如果你想对**指定主机**进行弱网环境下的测试,可以点击上图的“Add”按钮,在弹出的面板上设置协议、主机、端口来对指定的主机进行弱网设置。 ![设置指定网络请求的弱网模拟](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-102606.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 Proxy(HTTP)进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890 +- 将 Secure Web Proxy进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890 +- 将 SOCKS Proxy 进行勾选。另外在右侧的输入框填写 `127.0.0.1`,端口填写 ClashX 中查看到的端口信息,这里是 7890 + +这样我们的手机也具备科学上网能力啦。 + +Tips:iPhone 的 App Store 内置了苹果证书(Certificate pinning),所以抓包(也就是伪造证书)的方式是不行的。 ## 总结 Charles 功能强大、界面简洁,读完这篇文章并做出练习,相信你能很快掌握它,“工欲善其事,必先利其器” ,掌握了它,相信可以为你大大提高开发中调试网络的效率。**Enjoy yourself** -## 参考链接 -[唐巧的博客](http://blog.devtang.com/2015/11/14/charles-introduction/) diff --git a/assets/Charles-ExternalProxySettings.png b/assets/Charles-ExternalProxySettings.png new file mode 100644 index 0000000..9b91f1c Binary files /dev/null and b/assets/Charles-ExternalProxySettings.png differ diff --git a/assets/Clashx-Ports.png b/assets/Clashx-Ports.png new file mode 100644 index 0000000..84ed0b3 Binary files /dev/null and b/assets/Clashx-Ports.png differ diff --git a/assets/Iterm2-TestCase.png b/assets/Iterm2-TestCase.png new file mode 100644 index 0000000..33d63e3 Binary files /dev/null and b/assets/Iterm2-TestCase.png differ diff --git a/assets/UnitTest-FunctionStandard.png b/assets/UnitTest-FunctionStandard.png new file mode 100644 index 0000000..9a57ef7 Binary files /dev/null and b/assets/UnitTest-FunctionStandard.png differ