保姆级带你了解TDD
参考资料来源
- https://www.jianshu.com/p/e9e2191f267f
- https://blog.csdn.net/weiwei9363/article/details/117805722
- https://segmentfault.com/a/1190000010034142
- https://www.jianshu.com/p/8c39c6356c7b
简介
Test-Driven Development,测试驱动开发。
TDD是通过单元测试来驱动业务代码的实现。
要求开发者在进行逻辑实现前,优先进行测试用例的编写,站在调用者角度而非实现者角度去思考接口。
单元测试
简介
单元测试(Unit Testing)又称为模块测试,是针对程序模块来进行正确性检验的测试工作。单元就是单个程序、函数、过程等,编写程序的过程中前后很可能要进行多次单元测试。
然而在工作中,一般都是追求快速迭代上线,基本很难做到有时间来写测试用例。虽然大家都知道单元测试的优点,也非常认同,然而现实中确很难推行下去。
规范
单元测试编码规范:
- 测试类的命名应与被测试类保持一致,为“被测类名称+Test 后缀”。
- 测试方法表达业务或业务规则为目的。
- 测试方法体遵循 Given-When-Then 模式。
- Given: 为要测试的方法提供准备,包括创建被测试对象,为调用方法准备输入参数实参等;
- When: 调用被测试的方法,遵循单一职责原则,在一个测试方法的 When 部分,应该只有一条语句对被测方法进行调用;
- Then: 对被测方法调用后的结果进行预期验证。
优点
单元测试是一种验证行为
通过单元测试确实能够在一定程度上验证实现代码在特定情况下是否按照期望来运行相关逻辑
单元测试避免相当数量的反馈循环
开发中我们发现很多开发者在验证自己开发出来的功能时,会采用下面两种方案:
- 不测,凭直觉写完业务逻辑,直接丢给测试人员。
- 测,但是得通过UI界面上的操作,或者网络请求工具,或者临时编写个main函数再注释掉的方式来验证自己实现的业务代码。
第一种方法肯定是不可取的,因为反馈循环的链条最长
第二种,虽然开发人员进行测试,测试的目标代码和其他环境的依赖太多了。
单元测试也是一种编写文档的行为
UT和业务描述建立关系
如何让UT来为我们带来文档的作用呢?可以通过索引来实现,我们按照下面的格式为UserStory,Feature建立索引:
F=Feature
S=User Story
T=Task
然后为其加上序号
F01 S01 T01
或者
F01 S01
这样每一个测试方法通过索引都能够和业务文档上Task或者User Story的描述建立关系。
为了使UT更加易读,我们让测试方法名和测试方法块都按照given、when、then的方式来组织。
- Given:在什么亲体条件下
- When:发生什么事情是
- Then:结果是什么
通过Test Case我们能够清楚到找到业务上下文,并通过 Test Case 的方法名和方法体结构清晰的只要在测试什么内容。
单元测试是一种设计行为
先写测试时需要先组织测试条件和验证条件,最后写实现。
因为每次都是聚焦一小块业务代码的实现,对一小块代码进行添加、修改所以更容易划分清楚职责。
TDD和测试区别
TDD 很容易让人误解为就是先写测试后写代码。测试驱动开发,驱动的意思是由先进行软件方案设计,再写测试用例,最后写功能性的业务代码。这时候的测试用例可能就是一个方案设计的一个功能点,使用开发环境运行测试用例是失败的,因为功能代码还没开始实现。
重点在于我们在做测试驱动开发,而非测试。
用测试的技巧,你会全面的分析,并创建大量的测试来罗列不同的行为。
TDD则着力于代码设计,测试主要用于描述你要构建的行为,这些测试大都是TDD流程的附属产物,有了这些测试,在接下来改动代码的时候,你将更有信心。
TDD和传统开发模式区别
- 传统的开发模式:
- 功能点需求分析
- 根据功能点编码
- 反复测试
- 修正BUG
- 回归
- 功能上线
- 测试驱动开发模式:
- 功能点需求分析
- 根据功能点编写测试用例
- 根据测试用例进行编码
- 反复测试
- 修正BUG
- 回归
- 功能上线
用增量性地开发方式替代预先设计,这是TDD与传统开发方法最大的区别。
TDD开发周期
在做测试开发的时候,要重复一下简短的周期:
- 写一个测试(红)
- 让测试通过(绿)
- 优化设计(重构)
- 在红阶段,着力于系统行为的描述;
- 在绿阶段,要重点关注如何快速实现功能;
- 在重构阶段,关心如何提高代码的质量。
- 先分解任务,大需求拆分小需求,复杂逻辑细分,降低粒度。
- 写测试用例,聚焦于用例的注释和功能说明。
- 实现每个细分需求的功能代码。
- 完善单元测试用例,运行测试用例。
- 串联所有测试用例,通过集成测试。
- 重构并review代码。
简单的说,TDD的周期就是写一个测试,先确保测试失败,然后编码让其通过,接着审阅代码和打磨设计(包括测试的设计),最后保证所有测试仍然通过。在一天中,你不断的重复这个周期,保持周期尽量小,以便得到更多的反馈。
TDD优点
- 对需求负责(需求如何定义,测试用例就如何定义)
- 多个测试用例可精确描述所有的功能点
- 根据功能点,面向对象设计(对象设计,属性设计,接口设计)【设计的时候,会考虑到开发与到的问题】
- 编码的目的在于跑通一个个测试用例
- 测试用例相当于指南针,可以确保重构时和重构后,功能依旧能正确运行
- 新功能开发 ,继续添加新的测试用例,以保证新功能能在新的测试用例正常运行的同时,确保旧功能也能运行
- 测试代码要不断地更新,从最简单的测试代码到后来相对稳定的代码
- 测试代码是最好的接口文档,运行测试实例,可以从代码层面理解功能,减少沟通成本。
TDD规则
- 只在为了使失败的测试用例通过时才编写产品代码
- 当测试刚好失败时,停止继续编写测试。编译失败也是失败。
- 只编写刚好让一个失败测试用例通过的代码
测试的基本构成
3A
- 测试初始化(Arrage)
- 测试的行为(Act)
- 怎样验证结果(Assert)
有一个3A类似的助记词:Given-When-Then。
- 给定(Given)一个上下文
- 当(When)测试调用一些行为
- 然后(Then)验证结果
void test() {
// given
测试的条件
// when
mock数据
// then
assert断言
}
TDD的思维
增量性
可以用单元测试作为项目进度的衡量单位。逐个地处理单元测试,使用测试来定义和验证它们的行为,我们可以认为,任何没有测试描述的行为都没有实现,而经测试描述的行为则正确且完整地实现了。
以小步增量性地开发复杂的系统,每一步都以最简单粗暴的方法完成需求,并且通过重构来优化设计。
增量性的开发思维,减少了预先设计,将所有设计都做了拆分,每次在重构的时候就是一小次设计的过程。预先设计可能会让你陷入思维旋涡,为了追求设计的完美,你可能会思考每一种可能发生的情况,并期望设计出一种可以应对所有情况的软件,但通常预先设计是过度设计,甚至是错误的设计。
用增量性地开发方式替代预先设计,这是TDD与传统开发方法最大的区别。
测试行为而非方法
TDD初学者常会放一个错就是集中精力去测试成员函数,比如“我们实现了一个Add方法,然后写一个testAdd()测试”。
但是我们应该把注意力放在代码行为上。比如,如果加入一个重复的数据会怎样?如果客户传入一个空的数据呢?如果数据是无效的呢?我们就这几个考量做下面几个独立的测试。
使用测试来描述行为
- 测试名称,它概括了特定上下文中系统表现出的行为。
- 测试语句本身,它精炼的展示了一个测试行为。
必须保证其他人能够很容易的理解单元测试,否则你的单元测试就是在浪费别人的时间。
保持简单
当你写一个新的测试,来验证系统行为时,首先应该用你能想到最简单最粗暴的方式来实现这个功能。直到这个简单的方案无法满足新的需求时,再采用另一个稍微更复杂一点的方案来满足需求。
在实际的开发工程中,每次迭代都会交付新特性。最好的防范方法就是保持简单的设计:代码易读、没有冗余、没有所谓的复杂性。具有这些特性可以大大减少维护成本。
高质量测试
FIRST原则
- F,快速(Fast)
- I,独立(Isolated)
- R,可重复(Repeatable)
- S,自我验证(Self-verifing)
- T,及时(Timely)
快速
保持TDD的周期尽量短非常重要。如果测试需要三四秒来编译、链接和运行测试的话,你可以在短时间内获得很多反馈。
在单元测试中避免调用外部API,关于外部API的测试,应该被归为集成测试,而非单元测试。用Mock代替外部API调用。
独立
测试间是彼此独立的,使用静态数据或者全局数据的测试可能会因为旧的数据而失败。
应尽量避免在测试中引入静态数据或全局数据
可重复
高质量的测试是可重复的,一遍一遍的运行得到的结果总是相同的。
可能导致测试间隙性失败的原因
- 静态数据
- 不稳定的外部服务。以引入测试替身(Mock)来打破这种依赖。
- 程序并发
自我验证
自我验证就是采用自动化测试,而非手动测试。每个单元测试至少有一个断言,利用断言来完成自动化测试。不要在测试中加入 cout 或者日志来代替断言。
及时
及时编写测试意味着你要先写测试。同样,不要一开始写写一堆测试。相反,每次只写一个测试,尽量每个测试中只加入一个断言。
不适合TDD的开发场景
- 复杂算法
- 开发一些与物理现实世界交互的代码
- 难以构建测试的环境
TDD开发的例子
https://blog.csdn.net/qq_45661125/article/details/123478800
The end.