IOS-TDD总结

背景


最近在深挖如何提高单元测试覆盖率相关的东西。因为在实际实践项目单元测试的时候发现,编写完项目产品代码后发现再去编写单元测试方法,是个非常痛苦的事情。功能函数之间耦合严重,而且覆盖率上不来。即使使用了相关的依赖注入相关的方法重构了相关函数,大幅度改动也会影响现有的已经测试良好的功能代码,引入新的问题。覆盖率上不来的单元测试还不如人工测试。
在深挖如何提高单元测试覆盖率的时候看到了国外这一些列关于TDD的文章:

系列文章地址: [https://qualitycoding.org/tdd-sample-archives/](https://qualitycoding.org/tdd-sample-archives/)

总共30篇文章,讲的非常细致。
TDD只是一种编程的方式,这个方式比较容易理解。相当于一个盖房子的过程,一块测试代码砖一块相对应的能够通过测试的生产代码砖,然后再调整测试和产品代码结构。两个代码流同时进行,并非先砌完测试代码强再砌能够和它契合的产品代码墙,这样将会使契合的产品代码墙通过测试这个过程非常困难。
因为这个过程可能是测试代码写错了,也可能是产品代码需要几个过程才能完成配置,无法达到开发预期。
TDD的实施方式很简单,更多的是对开发者如何设计相互解耦单一原则等编码功底有着较高的要求。

一、实现TDD的步骤以及TDD的作用是什么?


TDD的主要作用应该是不同阶段给与开发者相应的及时反馈,而不仅仅是为了减少bug,这只是它的一个附属功能。

我们根据实施TDD的步骤:


  1. 编写单元测试代码。(先编写单元测试代码可以让开发者对接下来的产品代码要实现的功能有非常清楚的认识,同时这个过程种编写单元测试的难易程度也是API设计的是否合理非常好的反馈)
    这个过程主要是产生合理的API,因为还没写相关的产品实现,所以测试会不通过。
  2. 根据测试代码,编写能通过测试的产品实现代码。(产品代码是否符合测试代码的反馈)
  3. 重构测试代码与生产代码,这个过程的重构应尽可能简单。比如更改变量名、函数名、提取公用函数,非公用的函数封装成helper类等。 (这个过程需要始终保持单元测试通过状态,这个过程是对重构的及时反馈)。

清楚了TDD真正的作用,在实施时就不容易弄错。
注意TDD在实施的过程一定是测试代码与生产代码并行开发的,并且是step by step一步接一步的。相对于之前连续编写的方式有很大的不同,这也是实施TDD比较具有挑战性的地方。

为了不错乱,实施TDD可以遵循以下三个规则:

规则一: 
 先写测试代码再写产品代码
规则二:
 在编译不出错的情况编写单元测试代码
规则三:
  书写产品代码,代码足以通过当前的测试代码即可。

细看TDD的编写过程,更像是改一点运行下单元测试通过了,再改一点,再运行.......如此反复一步一步实现测试产品代码。因此单元测试慢到无法及时给与开发者反馈时,TDD便失去了意义。

单元测试应该具备的哪些特点?

1. 快速

A unit test that takes 1/10th of a second to run is a slow unit test.

因为TDD的实际实施过程,超过0.1s的单元测试都会被认为slow test。因为TDD的快速反馈特性,单元测试需要频繁运行。


比如这样单次只要0.001s,全工程71个单元测试只需0.7s。

怎么提高?

1.1 工程目录支持
1.1.1 添加测试TestingAppDelegate

单独的TestingAppDelegate类,可以在TDD的过程避开AppDelegate类冷启动的过程。

1.1.2 测试功能类单元测试类,放同一文件路径。


一个类对应一个单元测试文件,单元测试的类名以测试类名+test组成。测试类名可以单独运行,同时test和产品代码类在同一目录,方便其他同事了解,后期迭代时也可以跑相关的单元测试,确保没有影响到类的其他功能。

1.2 定位并处理运行慢的单元测试

关于这块APPCodeXctool 等IDE都提供了相应的工具支持。
实际的网络请求(异步)处理,可以在内网环境下进行快速的测试。
全部通过后,关闭网络再测试一次,然后将所有测试失败(依赖网络环境)的测试,全部移除到一个新的target 测试工程,并重新命名为网络测试或者验收测试。以保持主单元测试尽可能的快速;

而网络请求类的代码功能测试则可以通过mock类来进行单元测试:



比如这里的QCOFetchCharactersMarvelServiceTests测试类,传入的网络请求类其实是mock数据类,并不会真正发起网络请求。接下来的两个请求也是对网络请求类功能的测试。一个是验证QCOFetchCharactersMarvelService 是否正确的拼接了requstmodel的prefix参数到url,而另一个则是验证在调用fetch方法的时候, QCOFetchCharactersMarvelService 是否有调用sessionresume方法



小伙伴们也可以根据这个例子讲单元测试与验收测试区分开来(也称集成测试);
代码地址: MarvelBrowser

1.3 CI 持续集成工具支持

通过上面的代码区分整合,快速的单元测试将频繁的被调用,而慢的单元测试将会很少调用,甚至是忘记调用。
可以通过持续集成技术手段,来让机器处理这单一的工作。汇报测试异常,保证每次提交都完整的跑一遍单元测试,节省开发人员的测试时间。

2. 测试过程不需要开发者过多的干预

这里的干预是指单元测试参数校验错误,以及测试结果出错的问题定位不需要开发者插断点去调试。
一些比较好的单元测试断言三方库可以提供较好的支持。

  1. OCHamcrest
  2. OCMockito
  3. Quick/Nimble
    等。

2.1 单元测试的命名应该尽可能清晰

良好的命名规则应该包含以下内容:

1. What is being tested
2. Under what circumstances
3. What is the expected result

比如像这样

test_methodName_withCertainState_shouldDoSomething

虽然最终的名字会很长,但这将会让单元测试函数功能非常的清晰。
比如单元测试出错了,能够通过函数名,知道自己可能哪个地方出错了。而不需要插断点调试,或者联系代码上下文理解,理解需要测试的功能。
这个过程很浪费时间,记住我们要的是快速的反馈。

2.2 断言出错信息,清晰明了。

二、例子分析

2.1 TDD的方式实现Json数据解析类

| 视频地址:视频地址
| 文章地址:JSON Parsing: One of the Easiest Places for TDD Beginners
这章中### Jon Reid通过TDD方式编写了一个json解析类。比较具有代表性,TDD怎么写,看这节就行。
通过TDD与构造器模式书写的一个日常json数据解析类,使其完全覆盖实际使用场景:数据正常解析、 输入的数组不为空,但部分字段信息不全、输入的数组为空 等等情况。
json返回的数据结构如下:


根据json的层级,使用构造器设置的解析类结构如下。

不同的构造器负责解析json不同的层级的数据。各个部分相互独立,数据的组装比较灵活。比如:
正常的情况不必说,异常的情况比如:
异常一:输入的数组为空

异常二:角色对象缺失字段

非常方便。

单元测试:

正常情况



异常情况一 输入的数组为空



异常情况二 输入的数组不为空,但部分字段信息不全

2.2 TDD的实现三准则

| 视频地址:视频地址
| 文章地址:The 3 Laws of TDD: Focus on One Thing at a Time

Rules:

  1. You can’t write any production code until you have first written a failing unit test.
  2. You can’t write more of a unit test than is sufficient to fail, and not compiling is failing.
  3. You can’t write more production code than is sufficient to pass the currently failing unit test.

规则一:
先写测试代码再写产品代。
规则二:
在编译不出错的情况编写单元测试代码。
规则三:
书写产品代码,产品代码足以通过当前的测试代码即可。

三、需要的代码技能与相关书籍

TDD的三个阶段,需要的配备一些额外的编码技能主要集中在一三阶段。API设计与重构过程。
设计模式:

1.依赖注入与反向控制(DI and IoC)
依赖注入适用于模块之间解耦的一种设计模式。找了很久没有找到中文版的书籍。
《Dependency Injection》
《Dependency Injection in .net》
只有英文版本。第一本是基于java的可以看第一本。
或者简单点下面两篇博客
Intro to Inversion of Control and Dependency Injection with Spring
也可以看这篇Jon Reid根据IOS简化的依赖注入的方法
Dependency Injection

2.常用的设计模式


博客地址:https://blog.csdn.net/wiki_su/article/details/80263967
代码地址:https://github.com/WiKi123/DesignPattern
或者raywenderlich的这本 Design Patterns by tutorials

3.重构
重构现有的代码 《Refactoring: Improving the Design of Existing Code》
修改代码的艺术 《Working Effectively with Legacy Code》

四、扩展部分

4.1 编写单元测试时无从下手,如何让自己快速得到反馈?

无从下手时,不要停下来。可以在单前的开发分支上新建一个分支,在这个分支上任意发挥你想要写的测试。达到目的后删除分支即可。我们需要是快速反馈。

Spike Solutions: 7 Techniques You Can Use

4.2 验收测试

单元测试通过注入mock数据的方式,验证了类各个功能的正确性。
详见QCOFetchCharactersMarvelServiceTests测试类:
代码地址: MarvelBrowser
NSURLSession 以及 NSURLSessionDataTask 都是mock的对象,并非真正的网络请求,以及JSON Parsing 目录下的json解析类,能否正确的处理传入的网络请求model发起请求、json解析类能否正常的解析覆盖所有异常情况,都是验证类的正常功能。和集成测试(验收测试)相差的就是真实的网络请求。
上面有讲,这部分可以新建一个target测试工程并命名为验收测试。
每次提交开发人员自己运行或者放在CI集成平台,让集成平台完成集成测试的运行。

而赋值数据的UI是否正常显示,用户操作行为是否正常响应跳转。这又是另外一个范畴;
automated-testing

4.3 IDE支持以及相关工程配置支持

IDE: APPCode
告警config文件: https://github.com/jonreid/XcodeWarnings
config 相关的Rules说明地址:https://github.com/boredzo/Warnings-xcconfig/wiki/Warnings-Explained

推荐阅读更多精彩内容

  • 1.测试与软件模型 软件开发生命周期模型指的是软件开发全过程、活动和任务的结构性框架。软件项目的开发包括:需求、设...
    Mr希灵阅读 14,905评论 8 257
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 14,250评论 1 70
  • 时间:2016.12.10 地点:天府软件园A9 十分咖啡 the first 这次参加游戏共六个人,两个新玩家,...
    尧木启示录阅读 46评论 0 0
  • 一、GC收集器的分类: 按线程分为串行收集器和并行收集器,串行使用一个线程进行回收操作,并行使用多个线程进行回收,...
    城市里永远的学习者阅读 56评论 0 0
  • 我眼里的深圳 前些天闺蜜过来深圳办理入户,我陪了她大半天,她在广州工作,她过来一直说:“深圳的生活节奏太快,...
    蓝心14阅读 32评论 0 1