本篇文章纪录自己导入 测试驱动开发(Test Driven Design) 过程中,曾经没办法分辨自己所写的测试案例到底是“单元测试”还是“整合测试”,与同侪讨论后发现其他人也有相同的困扰。看了几本书与文章才釐清自己的问题所在,为方便与其他人进行交流讨论,故将自己理解的资讯整理下来并做个总结。
单元测试的涵盖範围很模糊?
单元测试(Unit Test)是软体开发中很重要的环节,替 TDD 提供重构的保护网,也是软体测试(Software Testing)中测试金字塔(Test Pyramid)的最低测试层级。
但是,一个「单元测试」所涵盖的範围到底有哪些,却让国外网友议论纷!
大家在初学单元测试一定会看到的定义如下:
以程式码的最小单位来进行正确性检验的测试工作,最小单位包括「类别与方法」。
若按此定义来写测试案例,一个单元测试只能包含一个类别。且受测类别的依赖都必须透过测试替身或 Mock 技术进行隔离,才能确保测试的目标是最小且不可分割的逻辑。
但是随着 Mock 的诟病被发掘(参考:Mock 不是测试的银弹),为避免 Mock 使测试案例变成开发人员的快乐表(测试通过,正式环境却出现错误),开始有人提倡使用 Spy 来替代 Mock,以及依赖若是自己的开发团队所写,而非第三方函式库,则可直接使用依赖。
这时,一个单元测试会执行的範围已经从 一个类别 变成 一个类别加上该类别的依赖。换句话说,一个单元测试除了受测程式外,也会执行到其他类别的程式码:
describe('AddGroupToRange', function () { it('空的统计範围, 将题组「questionGroups1」新增至空的统计範围中, 统计範围包含题组「questionGroups1」', function () { // @given 空的统计範围 var range = new StatisticsRange(); var pipeline = new Pipeline(range); // @when 将题组「questionGroups1」新增至空的统计範围中 pipeline.setRange(range); pipeline.addCommand(new AddGroupToRange('questionGroups1')); pipeline.run(); // @then 统计範围包含题组「questionGroups1」 expect(range.questionGroups).toEqual(['questionGroups1']); });});
如上範例所见,此测试案例已包含多个类别的逻辑。
但是,按照一开始所学的「单元测试定义」,我开始怀疑自己写的测试案例到底算不算单元测试呢?
原来单元测试涵盖範围有两派?
为解决疑虑,我到开始找人讨论、爬文试图找出单元测试的涵盖範围。最后在 Martin Fowler 的文章 UnitTest 找到答案,原来单元测试的涵盖範围有两派!
孤立型(Solitary)or 社交型(Sociable)
Martin Fowler^1 认为,在撰写单元测试时,搞清楚自己的测试案例属于 孤立型(Solitiary) 还是 社交型(Sociable) 很重要!
如果你喜欢使用 孤立型的单元测试,那么 受测物件将不会使用真实的依赖类别。因为依赖类别发生错误,也会造成单元测试无法通过!为了确保受测程式不被影响,孤立型单元测试 会利用测试替身(Test Doubles)模拟并隔离依赖(如图一右方)。
如果你喜欢 社交型的单元测试,则 受测物件会直接使用真实的依赖类别,让测试案例真实地执行一个完整的行为。
Martin Folwer 也提及,社交型单元测试的作法可能会因「单元测试的定义」而被抨击。但他觉得这并不是什么问题,他认为:
because these tests are tests of the behavior of a single unit.
单元测试是对一个行为的测试。
我们在测试一个行为时,也会「假设」受测行为以外的功能都是正常的。这种「假设」本质上与 孤立型的单元测试 是一样的!
(题外话:Martin Fowler 在文章中表明自己偏好社交型的单元测试)
TDD/BDD 是社交型单元测试吗?
在《修改软件的艺术》第 10 章测试先行,作者提及 TDD 的单元测试与狭义的单元测试不同,TDD 是以 一个行为 作为一个单元:
一个独立、可验证的行为。这个行为会对系统产生可观察的影响,且不和系统的其他行为耦合。
这个单元测试的定义意味着:每个可观察到的行为都应该要有一个相对应的测试。
另外在《Growing Object-Oriented Software, Guided by Tests》第五章节也指出,应该针对行为进行单元测试,而非针对方法。
这下真相大白了!如果你是 BDD 或 TDD 的实践者,那么你的单元测试就可能是跨多个类别的 社交型单元测试,因为测试的对象是 一个行为,而非一个类别。
TDD 并不能取代品质保证
TDD 所编写的测试,目的是为 系统重构(Refactoring) 提供支持。本质上与 QA 团队做的软体品质测试并不相同,因此狭义、细粒度 以品质保证为目标的单元测试 仍然有其存在的价值。
两种单元测试的差异:
社交型单元测试也算整合测试吗?
曾经我也有这个疑问,以为自己写的单元测试其实是整合测试吧?!
会有这种错觉,也是来自下面这条整合测试的定义:
对不同模组之间的交互作用进行测试但是测试案例成为整合测试的关键点是:测试案例是否包含与外部环境交互的逻辑,如时间、Session、Cookie、资料库,硬体,网路等等不受程式控制的因素。
简单来说,若测试案例无与外部环境交互的逻辑,则可以将测试案例视为单元测试:
反之,若测试案例中包含与外部环境交互的逻辑,那么这个测试案例就是一个整合测试:
[补充]
Uncle Bob 对 TDD 单元测试的看法:
单元测试的定义有两个版本,在国外好像越来越被接受了,但是国内却还不是很明确。
2017 年,Uncle Bob 在 Twitter 有对网友说明 TDD 单元测试的对象是一个“行为”,而非一个“方法”:
原文连结:
Uncle Bob 在 Twitter 的发言
最后,Uncle Bob 在后续留言还有补充 TDD 单元测试的测试案例应该写在哪个层级:
总结
TDD/BDD 与软体品质(QA)的单元测试很容易混淆,但两者的目的与涵盖範围并不相同。
若对两种单元测试的本质不够了解,就容易在写测试案例的时候陷入进退两难的窘境,因此釐清自己正在使用哪一种单元测试相当重要!若是带领一个开发团队,一定要在动手开发之前让团队要有一个统一的语言和定义。否则,做出来的结果可能相当不一样呢!