测试教案怎么写 怎么TDDTDD的基本流程是什么?(一)
这篇文章的结构:
什么是TDD
TDD有广义和狭义之分。常说的狭义是TDD,也就是UTDD(Unit Test Driven Development)。广义上的TDD就是ATDD(Acceptance Test Driven Development),包括BDD(Behavior Driven Development)和Consumer-Driven Contracts Development。
本文提到的TDD是指狭义的TDD,即“单元测试驱动开发”。
TDD 是敏捷开发的核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前先写单元测试用例代码,测试代码决定了需要写什么产品代码。TDD 是 XP(极限编程)的核心实践。它的主要推动者是肯特贝克。
TDD具有三个含义:
为什么 TDD 传统编码 VS TDD 编码
传统编码
TDD编码方式
TDD 的好处
减轻开发者的负担
通过一个清晰的过程,让我们一次只关注一个点,思考的负担就少了。
防护网
TDD的优势在于它涵盖了完整的单元测试,并为产品代码提供了保护网,让我们可以轻松应对需求变化或改进代码设计。
所以如果你的项目需求稳定,一气呵成,不做任何后续改动,你就能少享受到TDD带来的好处。
提前明确要求
先写测试可以帮助我们提前思考需求,明确需求的细节,而不是在代码写到一半时才发现不清楚的需求。
快速反馈
很多人讲TDD的时候,我的代码量增加了,所以开发效率降低了。但是,如果没有单元测试,则必须手动测试。你要花很多时间准备数据、启动应用程序、跳转到界面等等,而且反馈很慢。准确地说,快速反馈是单元测试的好处。
TDD怎么样
TDD
TDD的基本流程是:红、绿、重构。
更详细的过程是:
你可能会问,如果我写一个测试用例,它显然会失败,我必须运行它吗?
是的。你可能认为测试只有成功和失败两种情况。然而,失败的种类数不胜数。只有运行测试才能确保当前的故障是您期望的故障。
一切都是为了让程序达到预期,这样当出现错误时,可以快速定位错误(肯定是刚刚修改的代码引起的,因为一分钟前的代码符合我的预期)。
这样就节省了大量调试代码的时间。
TDD 的三个规则不允许编写任何产品代码,除非它是通过失败的单元测试。在单元测试中,只允许写入可能导致失败的内容(编译错误也视为失败)。通过失败的单元测试通过的产品代码
如果违反了会怎样?
违反第一条,先写产品代码。这段代码需要实现什么?如何保证真正实现?
违反第二条,写了多次失败的测试。如果长时间测试失败,会增加开发者的压力。此外,测试可能会被重构,这会增加测试的修改成本。
违反第三条,产品代码实现了超出当前测试的功能,则这部分代码不受测试保护,不知道正确与否,需要人工测试。也许这是一个不存在的需求,它凭空增加了代码的复杂性。如果是已经存在的需求,那么下面的测试就直接通过了,破坏了TDD的节奏。
我认为它的本质是:
分开关注,一次只戴一顶帽子
在我们的编程过程中,有几个关注点:需求、实现和设计。
TDD 为我们提供了三个明确的步骤,每个步骤都侧重于一个方面。
红色:编写失败的测试。它是对一个小需求的描述。你只需要关心输入和输出。此时,您无需关心如何实现它。
绿色:专注于以最快的方式实现当前的小需求测试教案怎么写,不关心其他需求,也不关心代码质量有多糟糕。
重构:不需要考虑需求,也没有实现的压力,只需找出代码中的异味,并用一种方法消除它,使代码成为干净的代码。
注意力控制
人们的注意力可以被主动控制或被动吸引。如果你来回转换你的注意力,你会消耗更多的能量,你的思维也会不够完整。
使用TDD开发,我们需要主动控制attention。写测试的时候发现没有定义一个类,IDE提示编译错误。这时候,如果你创建了这个类,你的注意力就不会放在需求上,已经切换到了实现方面,我们应该集中精力写这个测试,想想它是否表达了需求,开始消除确认无误后编译出错。
为什么很多人做不了TDD?
不会合理拆分任务
在TDD之前,任务是拆分的,一个大需求被分成多个小需求。
您还可以拆分多个功能。
不能写测试
什么是有效的单元测试?很多人写测试。他们甚至不知道正在测试什么,或者他们甚至没有断言。它们可以通过控制台输出和视觉比较来验证。
一个好的单元测试应该遵循几个原则:
不会只写实现
很多人在写实现的时候不能专注于当前的需求,不小心实现了其他的需求,破坏了节奏感。
当谈到实现时,它不会采取小步骤。
不会重构
我不知道 Clean Code 是什么,我没有看到 Smell,我也没有及时重构它。当我想重构时很难开始。
我不知道如何用适当的“技巧”来消除气味。
基础设施
对于特定的技术栈,单元测试基础设施没有搭建好,导致在编写测试时无法专注于测试用例。
实例
编写一个程序来计算文本文件words.txt 中每个单词的出现频率。
为简单起见,假设:
例如,假设 words.txt 包含以下内容:
天气晴朗
晴天是
您的程序应输出以下内容,按频率的相反顺序排序:
4
是 3
晴天2
第一天
请不要往下读,想想你会怎么做。
(思考3分钟……)
当一个新手得到这样的需求时,他会把所有的代码写在一个 main() 方法中。伪代码如下:
main() {
// 读取文件
...
// 分隔单词
...
// 分组
...
// 倒序排序
...
// 拼接字符串
...
// 打印
...
}
思路很清晰,但往往是一口气完成,最后运行,但输出不符合预期,然后开始断点调试。
这段代码没有任何封装。这也是为什么很多人在听说有的公司把一个方法限制在10行以内的时候,立马跳出来说这是不可能的。10行可以做什么?我们的业务逻辑很复杂...
这样的代码有什么问题?
好吧,让我们来谈谈 TDD。读取文件和输出控制台的测试代码是怎么写的?
当然,我们可以通过Mock和Stub来隔离IO,但是真的有必要吗?
有人问 Kent Beck 这个问题:
你真的会测试一切吗?甚至会测试 getter 和 setter 吗?
Kent Beck 说:公司邀请我去实现商业价值,而不是写测试代码。
所以我只在我没有信心的地方写测试代码。
对于我们的程序来说,读取文件和打印到控制台都是调用系统API,所以你可以很放心。最不自信的是中间的业务逻辑。
所以我们可以对程序做一些封装。根据“代码清洁度”,可以在有注释的地方提取方法,可以用方法名代替注释:
main() {
String words = read_file('words.txt')
String[] wordArray = split(words)
Map frequency = group(wordArray)
sort(frequency)
String output = format(frequency)
print(output)
}
是否可以分别为 split、group、sort 和 format 编写单元测试?
当然,他们的输入输出是非常清晰的。
等等,你可能会说,这不是测试驱动设计吗?你是如何开始设计的?好问题!
TDD应该提前设计吗?
Kent Beck 没有提前设计,他会选择最简单的用例,直接写出来,用最简单的代码通过测试。逐渐增加测试,使代码更复杂,并使用重构来驱动设计。
在这个需求中,最简单的场景是什么?
即文件内容为空,输出也为空。
当然,对于复杂的问题,你可能会在编写的时候添加新的用例,但是对于这么简单的话题,你基本上可以提前考虑什么用例会驱动什么产品代码。
您可能会想到以下用例:
Martin Fowler 的观点是,在我们编写代码来做 Big Front Up Design 之前,我们必须在开始编写代码之前设计所有细节。
有了重构工具之后,设计的压力就小了很多,因为有测试代码保护,我们可以随时重构实现。但这并不意味着我们不需要提前设计。提前设计可以让我们与他人讨论,并在开始编写代码之前迭代几次。在纸上迭代总是比更改代码更快。
我个人很认同 Martin Fowler 的做法,先在脑子里设计(当然我脑子不够用,所以我用纸来画),经过几次迭代后开始写作。这样,我还是会用最简单的实现来通过测试。但是重构的时候有方向,效率更高。
回到这个程序,我发现当前的包不是抽象的。更理想的设计是:
分解任务
main() {
String words = read_file('words.txt')
String output = word_frequency(words)
print(output)
}
word_frequency(words) {
String[] wordArray = split(words)
Map frequency = group(wordArray)
sort(frequency)
return format(frequency)
}
这个时候,还有两个选择。有些人喜欢自上而下,有些人喜欢自下而上,我个人更喜欢前者。
从现在开始,只需遵循红绿重构循环即可。
大多数TDD都做的不好,也就是没有之前的任务分解和Example列举的过程。
想看TDD过程可以参考我的直播。
或者,如有必要,我也可以录制有关此主题的视频。
常问问题
为什么要先写测试测试教案怎么写,后补测试?
OK,但是写完实现后,马上写一个测试,用测试来验证实现。如果先手动测试,调试代码,再添加单元测试,会觉得很鸡肋,增加工作量。
无论您是先测试还是后测试,您都可以享受快速反馈,但如果您先测试,您还可以享受另一个好处,即使用意图驱动的编程来减少返工。因为你的测试代码是产品代码的客户端(调用者),你可以随意编写测试代码(方法名、参数、返回值等),然后实现产品代码,而不是先写实现然后写Testing,前者返工少。
我刚刚写了一个测试,但还没有写一个实现。明知道运行测试会报错,为什么还要运行呢?
其实,测试的结果不仅仅是通过和失败,因为失败的可能性有很多。所以如果知道肯定失败,就运行测试,目的是看是否报了预期的错误。
小步子固然好,但你真的需要这么小步子吗?
台阶太大,容易拉鸡蛋。
练习时需要养成小步的习惯,工作时可以自由切换步的大小。
当您有信心时,您可以采取更长的步骤。当你没有信心时,你可以立即切换到小步模式。如果只能迈出大步,就很难迈出小步。
测试代码会成为维护负担吗?
在维护时,我们也遵循TDD流程,先修改测试代码看需求变化,让测试失败,再修改产品代码通过。
这样你就不用维护测试用例,而是使用测试用例。
为什么要快速实施?
实际上,二分查找法是用来隔离问题的。测试通过硬编码后,基本确定测试没有问题,然后实现产品代码。如果测试失败,则是产品代码问题。
所以这小步主要是隔离问题,也就是可以和Debug说再见了。
为什么测试代码应该简单?
如果一个测试失败,修复的时候改的是测试代码而不是产品代码,也就是测试代码写的不好。
当测试代码足够简单时,如果测试失败,您就有足够的信心得出结论,它一定是产品代码有问题。
什么时候不适合TDD?
如果您正在进行不需要长期维护的探索性技术研究(Spike),并且测试基础设施的构建成本很高,那么最好进行手动测试。
此外,还有“可测试性极差的遗留系统”和“使用对测试不友好的技术堆栈”的系统。TDD 可能会超过收益。
学习路径 《有效的单元测试》 《代码清洁》 《重构》 转型优先前提 《以测试为导向的开发实例》 《以测试为导向的成长型面向对象软件》扩展阅读
本文最初发表于 GitChat。文章已免费发表。欢迎购买问答记录。
虚构诋毁攻击竞争对手