我读过很多关于单元测试的帖子,它们只测试一个对象/类,并且对象的模拟应该只针对被测对象的直接依赖项。讨论的测试的唯一其他选项是完整集成测试。
为什么不鼓励进行更高级别的“单元”测试,即一次测试多个对象?如果一个应用程序有组件 A、B 和 C,并且该应用程序应该执行 X、Y 和 Z,那么就为 X、Y 和 Z 创建测试。这将锻炼 A、B 和 C 组件的代码。
如果 A、B 或 C 中的代码未被 X、Y 和 Z 测试覆盖,则要么不需要该代码,要么应添加额外的高级测试。如果需要,可以添加 A、B 或 C 的低级单元测试来增强 X、Y 和 Z 测试。
单独对 A、B 和 C 进行单元测试会产生大量重复工作,并且很难重构代码。在确定错误位置方面可能会有些不准确,但使用调试器通常可以相当快地诊断问题。此外,如果需要,可以实施较低级别的测试。为什么测试只能是单个对象单元测试和完整的应用程序集成测试?
6
5 个回答
5
我读过很多关于单元测试的帖子,它们只测试一个对象/类,并且对象的模拟应该只针对被测对象的直接依赖项。讨论的测试的唯一其他选项是完整集成测试。
这种风格是错误的。很抱歉,但确实如此。哦,当然我可以说,好吧,这是一种方法。不,不是。这很糟糕。我知道,因为我曾在坚持这种风格的商店工作过,这是面向对象编程的消亡。在认真对待单元测试覆盖率的同时这样做,您将编写过程代码,只是为了避免创建另一个必须测试的对象。
Fowler 曾写过关于文章。他称这些测试为“单独测试”,而坚持这种测试的风格则是“模拟主义”。他很清楚这不是他的风格。
现在我并不反对偶尔编写单独的测试。它的所有依赖项都被模拟,因此不会发生任何不是被测试对象错误的事情。但坚持以这种方式测试每个对象是结构化思维而非行为思维的可恶行为。以这种方式测试每个对象,就像把胶水倒在乐高积木上一样。
正如您不应该通过访问修饰符来测试私有方法一样,您也不应该通过 API 来测试私有对象。
为什么?因为抽象是一件好事。让我把东西隐藏起来,只要你能得到你需要的行为,即使我改变它的工作方式也没关系。这给了我们改变事物的自由,不仅是在对象内部,而且在对象之间。只要它做同样的事情,谁在乎呢?
的存在本身就证明了一个对象可以抽象许多其他对象。这些其他对象应该像任何私有方法一样具有隐私性。
为什么不鼓励进行一次测试多个对象的更高级别的“单元”测试?
因为你还没有认真寻找。福勒鼓励这样做。。。我也在鼓励这样做。那些不这样做的人往往最终会变成卖嘲讽工具的人。
另一方可能会争辩说:如果您一次测试多个对象,那么您正在进行某种形式的集成测试。
对此,我想问的是,对象为何如此特殊,以至于只有当我们集成它们时我们才会关心它们?一些可测试的语言完全不需要对象就可以很好地运行。或者我们是否只需要将任何与任何东西集成的测试称为集成测试?如果是这样,您的方法最好只有一行代码。或者您需要模拟其他代码行。
抱歉,但这种思维方式之所以流行,只是因为它使得在代码审查中无脑地批评某人变得容易。“嘿,你添加了一个Foo
类。你的类在哪里FooTest
”。呃。
那么,集成测试是什么?它是一种没有精心划分的可测试单元的测试。应该是确定性代码(所以没有 IO)。单元应该很快(所以没有 IO)。单元应该易于运行(所以不需要我编辑配置文件)。单元应该是可以同时运行多个测试的东西(没有奇怪的副作用)。简而言之,单元是你的功能代码。你可以随意划分得粗细。对象真的与它无关。
以这种方式编写可测试的代码并非毫无代价。这是工作。您最终遵循的模式有一个名称:。
这种模式承认有些东西不适合功能核心。有些东西超出了功能核心。有时这些东西需要测试。这就是你进行集成测试的原因。
2
-
强调没有 IO 有点太难了。如果 IO 由框架管理,那么删除 IO 的唯一方法就是模拟框架,这通常是不切实际的。然后我使用 IO 编写集成测试,并经常从中得到有用的失败。是的,由框架定义的架构很糟糕,但模拟框架是无用的——它永远不够详细,无法检测到复杂的交互错误。
–
-
3@Basilevs 你想在测试中加入 IO?没问题。就叫它集成测试吧。要有勇气清楚地标记事物。
–
|
这是一个很大的话题,所以我只会在这里写适量的文字,并引导您参考外部资料以供进一步阅读。
披露:我是外部来源的作者。
当今业界实施自动化软件测试的方式涉及使用模拟对象进行单元测试。根据既定做法,这三个概念被视为密不可分地联系在一起:
- 如果您想要进行自动化软件测试,那么您必须进行单元测试。
- 如果您想要进行单元测试,则必须使用模拟对象。
这正是全世界绝大多数程序员所做的事情。
这一切都是极其错误的。
单元测试存在的唯一原因是为了实现缺陷定位:当系统出现故障时,我们希望确切地知道哪个组件导致了故障。
缺陷定位实际上是硬件测试中的一个大问题,单元测试的概念就是从硬件测试中产生的。它在软件测试中也很重要,但远没有那么重要。尽管如此,单元测试是人们唯一知道的工具,所以人们就是这么做的。
为了实现缺陷定位,单元测试要求每个组件必须与系统的其他部分严格隔离地进行测试。为了实现这一点,人们使用模拟对象。使用模拟对象是错误的,因为它们费力、过度指定、冒昧,并且会导致测试脆弱且不可重用。有关更多信息,请参阅
以下是该论文的摘要:
文章从批判性的角度审视了在自动化软件测试中使用模拟对象的做法,发现其存在很大问题。文中引用了一些知名行业发言人的观点。文章表明,模拟对象所谓的好处要么没有实际好处,要么可以通过其他方式实现。
(注:引用的知名行业发言人包括 Martin Fowler 和 Kent Beck。)
因此,您在问题中提到的“一次测试多个对象的高级单元测试”并不存在,因为它不是单元测试。如果您一次测试多个对象,那么您正在进行某种形式的集成测试,如果您同时测试所有对象,那么您正在进行端到端测试。
我想到了一种替代技术,它在实现缺陷定位方面大有裨益;只是没有完美实现,但有其他好处。我称这种技术为增量集成测试。也许它接近于您一次测试多个对象的概念。您可以在此处阅读:
以下是该论文的摘要:
本文介绍了一种新的自动化软件测试方法,作为单元测试的替代方案。新方法保留了单元测试的优点,即缺陷定位,但消除了模拟的需要,从而大大减少了编写和维护测试的工作量。
很难说清为什么人们坚持单元测试。也许是对传统的坚持,也许是对货物崇拜工程的坚持,也许是不愿承认他们之前犯下的谬误的严重性,也不愿放弃他们已经为此付出的巨大努力。
2
-
1“缺陷定位”是我需要记住的一个流行词。我一直认为单元测试的最大好处是在它失败时实现,而不是在它通过时实现。
– -
我知道我之前在这个网站上遇到过这个问题,但是你的回答让我想起了社交与孤立单元测试(尽管我目前还想不到具体的参考资料)。
–
|
对于单元测试,存在多个学派。
在Martin Fowler替身来隔离单元(方法或类单元测试)。
其中很多内容可能比您想象的要低。如果应用程序应该执行 X、Y 和 Z,那么这些内容可能不会通过单元测试进行测试。应用程序将通过我称之为“验收测试”或“场景”的东西进行测试。我倾向于将单元测试视为面向开发人员和技术而不是面向用户的测试,这更符合描述。
2
-
更好的术语是功能测试。验收测试从定义上来说不能自动化,场景是一个模糊的术语。
– -
@Basilevs 应用程序级测试不必局限于功能。例如,测试可以断言性能,如时间或内存消耗。我不明白为什么验收测试不能自动化——可能存在特定类别,例如可用性或可访问性测试,只能在有限的范围内实现自动化(例如根据明确定义的规则进行检查)。Gherkin 等工具使用术语“场景”来指代包含要测试步骤的业务规则的说明,因此我看到它渗透到其他测试术语中。
–
♦
|
您描述的实际上是经典的单元测试。在经典的单元测试中,您只在必要时模拟依赖项,例如,如果依赖项是不确定的或不切实际的缓慢。
相比之下,有一种“模拟主义”风格,也称为伦敦学派,它坚持认为所有对被测试类的依赖都应该被模拟。
马丁·福勒 (Martin Fowler) 这样描述这两种思想流派。
这里最大的问题是何时使用模拟(或其他替代)。
经典的 TDD风格是尽可能使用真实对象,如果使用真实对象不方便,则使用双精度。因此,经典的 TDDer 会使用真实的仓库和双精度来提供邮件服务。双精度的类型其实并不那么重要。
然而,模拟TDD实践者总是会对具有有趣行为的任何对象使用模拟。在本例中,仓库和邮件服务都是如此。
正如您所观察到的,模拟方法有许多缺点。从根本上讲,它需要做更多的工作,但会导致测试更脆弱、更不彻底。模拟方法的一个优点是,当测试失败时,很容易找出错误,因为测试只涉及一个类。
|
的定义,中间会有一些层次
- 测试单个代码单元,例如 Python 模块或 Java 类;这就是上面所说的“单元测试”
- 测试不同模块之间的交互
- 在不依赖其他系统的情况下测试系统
- 将多个系统集成在一起;这就是上面所说的“集成测试”
如果你只知道一把锤子,那么所有东西看起来都像钉子。
|
–
–
–
–
–
|