1. 反馈
反馈是我们认识世界常用的方法。 我们先有一个模型(假设),这个模型去解释现象,然后再结合实践或者实验,得到的实际结果与模型得到的期望结构对比。如果一致,那么我们认知与外部世界一致;如果有差异,那么模型有误,需要修正我们的大脑里面认知。 反馈帮助我们发现问题,快速反馈可以帮助尽早的发现问题,从而更加容易纠错。 如果缺乏这种快速反馈机制,最终出错了,到底哪里出错,排错的难度就大很多了。 所以有很多种方法论都利用反馈机制。
比如戴明环,PDCA:
比如开发MVP模型:
比如敏捷开发迭代模型:
基于这种反馈机制,我们每一次迭代,都通过反馈来检验,后面的迭代以前面为基础。 随着迭代次数增加,构建模型也越来复杂,甚至复杂超出我们大脑容量(比如大型软件不可能一次全部放入大脑去思考)。这个时候对于模型的修改,很难发现问题。 潜在的思考不周全,状态行为不一致,引入bug,甚至regression的风险就很大。 但是如果对于每一次修改,都有反馈机制辅助我们来检验,那么风险就降低很多了。 基于构建的模型,可以认为是看做是生长的,一点一点长大,所谓基于反馈增长模型;
另外一种则是线性的增长,线性模型(差之毫厘,谬以千里)
比如说瀑布开发模型中,在需求源头中有错误,在最终发布在客户那里发现,那么这时候再纠正,那难度和成本与在开始发现时候相比,真是数量级的差异。
在软件开发中,往往基于反馈比这种线性开发方式更容易控制。
2. 软件开发中反馈
2.1 敏捷宣言遵循的12条原则,也是反馈具体表现。
我们最重要的目标,是通过持续不断地早交付有价值的软件使客户满意。
经常地交付可工作的软件,相隔几星期或一两个月,倾向于采取较短的周期。
业务人员和开发人员必须相互合作,项目中的每一天都不例外。
可工作的软件是进度的首要度量标准
2.2 敏捷开发Scrum。 下面图中每一个环路都是一个反馈。每一次反馈就是一次尽早发现问题的机会。
2.3 敏捷里面的工程实践
比如CI/CD管道,对于每一次代码的更改,甚至是配置文件的更改,对需要走一次反馈。 从代码扫描,编译,测试,部署, 每一步都是一次反馈。
比如团队之间反馈活动,code review, code inspection and pair programming。
再比如测试本身就是一种反馈。经典的测试金字塔( 单元测试,模块测试,系统测试,探索性测试)都是不同层面的反馈,只是粒度不一样。
TDD 也是一种反馈。 先写测试用例,然后写代码;运行测试,去验证代码,这是一次反馈;重构代码,运行测试,也是一种反馈。
3. 有效反馈的特点(早, 快,准确,可重复 )
反馈可以帮我们发现问题,从而辅助我们纠正问题。 但是不是所有的反馈都是有效的。 比如说反馈时间太长,耽搁下一次迭代的时间;如果等不及反馈结果进行下一次迭代, 如果出错,不知道是这次迭代还是上次迭代出的错。反馈不准确,得到不有有效信息排错,丢到反馈部分价值; 反馈结果不重复,那么反馈不可靠,就会干扰正常工作。 所以有效的反馈,需要有以下4个特点。
反馈早, 反馈的快,反馈的准确,反馈的可重复
早点得到反馈结果,如果有错,这个时候修改距离上次修改比较少,在时间上距离上次修改间隔比较短,容易排查错误,而且成本低。 只有反馈的快,反馈的结果才可以早点拿到。 反馈的准确性给予足够的信息,可以方便我们排查纠错。而反馈结果的可重复性,一致性,才使得反馈有意义。
下面从这四个方面考察TDD中测试用如何做到。 当然这些原则对于其他软件实践也同样适用,如CI/CD,pair programming, 和软件测试。
4. TDD 中如何构建有效的反馈
4.1 早反馈:
TDD是先写测试用例,再写代码,也就是test-first 。在没有写代码之前已经建立好反馈机制,与其他各种实践在代码写之后再去验证,而言有更大的优势。 写测试用例,完全不用去考虑实现代码,实现代码压根不存在,这样写出的代码不受实现 影响,是一个黑盒子,测试与实现彻底解耦。这样可以写出来的测试没有实现约束干扰,更可以表达出来意图。
4.2. 快反馈
反馈结果要拿的早早,尽量减少测试执行的时间。
计算机世界,要快当然是自动化, 自动化可以减少人为干扰。彻底自动化,一键式自动化,一个命令所有的东西都运行。现在主流的语言就是这样。比如maven,gradlle.
另外测试用例执行速度一定快,那么就要去掉或者减少对外部依赖延时;比如数据库,大文件读写,网络;这些可以用test dump技术,dump/mock 去替换现实世界中依赖。
增加机器,并行执行。如果当test case 很大的情况,并行执行,加速执行过程;
测试影响分析; 但是还有另外一个思路,每一次代码的修改,不需要运行所有的测试用例;那么怎么选测试用例,使得不多不少,从而减少哪些不需要的测试用例的执行,也可以大大的减少执行的时间。参见,减少无用测试用例的执行,从而提高执行速度。
4.3 准确反馈 -- 减少干扰
测试如果出错,要很明白的告诉哪里出错,这样很容易帮助我们定位问题;而不是含糊其词unknown error,segment error。所谓准确,就是当测试失败的时候,下面这些信息是很容易得到的
1. 这个测试用例测试什么场景?
2. 这个测试用例的测试点是什么?
3. 这个测试用例的输入,输出,和期望输出是什么?
4. 这个输出与期望输出,有哪些不同?
如果这些信息都可以很准确的获得,如果刚修改的代码上下文还没有忘记,大概就可以推测出代码出错的地方。 不就是刚才修改的地方?
但是这里有一个前提是,测试用例一定容易理解。那么怎么才能容易理解? 测试名字,需要认真考虑,需要体现测试场景和意图; 测试点,可以用assert来显示刻画。理想情况下一个测试用例一个测试点assert。如果有太多的,那么测试意图就不明显。这个时候应该考虑将这个测试用例分解多个测试用例,不同的关注点不一样。测试用例也需要考虑职责单一原则。
更进一步,结构化的写测试用例可以帮我们理解。 结构化的测试用例分四部分:设置参数,调用方法,验证期望,清理现场。 对于每一个测试用类似的结构化模板统一写,方便查找和理解。
再进一步,如果有准确的错误信息那么重要,为什么不利用反馈机制让错位更清晰准确? 那么 TDD可以变成这样样子:
4.4 重复反馈 -- 环境隔离
重复,这里指的是如果测试结果一致性,每一次运行结构都一样。 对于不同人,不同机器,不同的时间,运行结果是一致的。 这样的测试结果才是可靠的,所有人都信任的。 这样较少误报,减少干扰。
如果测试结果不一致,大多是是外部环境导致的。比如随机说,时间,网络,第三服务,操作系统等等都可能产生潜在的不一致。 一方面减少对环境的依赖,另外一方面使得测试环境标准化。虚拟机或者容器很大程度帮助我们减轻环境的不一致。
5 小步慢走,和快速反馈和回退重来相结合。
小步慢走,分解是核心;将一个大的task分解为小的任务,对于每一个小任务更好的控制。 快速反馈,可以让我们及时发现偏差; 同时, 快速反馈机制,可以 帮你监控regression,减轻大脑负担,让大脑专注于当前的工作。 如果出错,因为每一次改的小,排查错误的范围小; 同时反馈的及时,在大脑没有遗忘上下文的时候就可以很快展开纠偏工作,高效。 另外,如果每一次的小步都有保存,那么我们还有一个回退按钮,给我重来的机会。
将三者相结合,这样的方式去写代码,就可以鼓励去修改,大胆的修改,放心的修改,有信心去修改,终于奔向自由飞翔了。
之前提到过一个工具cyber-dojo, 刻意练习TDD工具。集成 小步快走(分解),快速反馈(TDD),同时任何时候运行会保存下来,提供回退机会。 如果刻意练习,应该尝试一下。