单元测试的采用


69

我们已经尝试将单元测试引入当前的项目中,但是似乎没有用。额外的代码似乎已经成为维护工作的头疼问题,因为当内部框架发生更改时,我们必须四处解决并修复所有与之相关的单元测试。

我们有一个用于对控制器进行单元测试的抽象基类,它充当调用子类的抽象方法实现的模板,即,框架调用Initialize,因此我们的控制器类都有自己的Initialize方法。

我曾经是单元测试的拥护者,但是它似乎不适用于我们当前的项目。

谁能帮助您确定问题以及我们如何使单元测试对我们有用而不是不利于我们?


有趣的问题。请告知我们!
09年

Answers:


108

提示:

避免编写程序代码

如果测试是针对严重依赖全局状态或深陷于丑陋方法中的程序式代码编写的,则可能会让人难以忍受。如果您使用OO语言编写代码,请有效使用OO构造来减少这种情况。

  • 尽可能避免使用全局状态。
  • 避免使用静态变量,因为它们往往会在您的代码库中波动,并最终导致某些事情本来应该是静态的。它们还会夸大您的测试环境(请参见下文)。
  • 有效利用多态性来防止过多的if和flags

找到什么变化,将其封装并与保持不变的部分分开。

代码中的阻塞点比其他部分更频繁地更改。在您的代码库中执行此操作,您的测试将变得更加健康。

  • 良好的封装导致良好的,松散耦合的设计。
  • 重构和模块化。
  • 保持测试小而集中。

围绕测试的上下文越大,维护起来就越困难。

竭尽所能缩小测试范围以及执行测试的周围环境。

  • 使用组合方法重构来测试较小的代码块。
  • 您是否正在使用更新的测试框架,例如TestNG或JUnit4?它们通过为您提供测试生命周期中的更多细粒度的挂钩,使您可以消除测试中的重复项。
  • 使用测试双打(模拟,假冒,存根)进行调查以减小测试上下文的大小。
  • 研究“测试数据构建器”模式。

从测试中删除重复项,但确保它们保持焦点。

您可能无法删除所有重复项,但仍尝试在引起痛苦的地方将其删除。确保您没有删除太多重复项,以至于某人无法进入并一目了然地告诉测试内容。(有关同一概念的另一种解释,请参阅Paul Wheaton的“邪恶的单元测试”文章。)

  • 如果他们不知道自己在做什么,谁也不想修复测试。
  • 遵循编排,执行,断言模式。
  • 每个测试仅使用一个断言。

在您要验证的正确级别上进行测试。

考虑一下Selenium记录和回放测试所涉及的复杂性,以及与测试单个方法相比,您可能会发生的变化。

  • 相互隔离依赖关系。
  • 使用依赖项注入/控件反转。
  • 使用测试双精度来初始化要测试的对象,并确保您正在隔离地测试单个代码单元。
  • 确保您正在编写相关的测试
    • 通过有意引入一个错误并确保它被测试捕获来“弹跳陷阱”。
  • 另请参阅:集成测试是骗局

知道何时使用基于状态的测试与基于交互的测试

真正的单元测试需要真正的隔离。单元测试不会命中数据库或打开套接字。不要嘲笑这些互动。验证您是否与协作者正确交谈,而不是此方法调用的正确结果是“ 42”。

演示测试驱动代码

一个特定的团队是否将测试所有代码进行测试,还是为每行代码编写“测试优先”,尚有待商debate。但是他们应该至少首先编写一些测试吗?绝对。在某些情况下,首先测试无疑是解决问题的最佳方法。

资源:


1
为什么这个问题没有得到更多支持?这是一些好东西!
Epaga

1
这是我的一个存根博客条目。我可能会重申这个问题并尽快发布。
cwash

2
认真地说,每个测试一个主张?您花费多少时间编写测试方法签名?
erikkallen

1
另外,为什么不喜欢静态方法呢?我经常发现测试一堆静态方法(当然,没有副作用),然后将变异实例方法的形式设置为“ mem = f1(mem1,mem2); mem2 = f2(mem1)”,这通常要容易得多;“,然后仅测试静态方法。
erikkallen

1
@erikkallen-w / r / t静态方法,静态方法是一个黑匣子。我不能出于测试目的更改任何内容。它们倾向于传播全局状态的使用,因此使测试更加困难。如果编写的静态方法不使用全局状态,则该方法中使用的所有状态都是通过参数传入的。为什么不将其写为实例方法呢?再次参阅:googletesting.blogspot.com/2008/12/…,这不是宇宙定律,如果您关注可测试性或可维护性测试,则是个好主意。
cwash

19

您是否正在测试足够小的代码单元?除非您从根本上更改核心代码中的所有内容,否则您应该不会看到太多更改。

一旦一切稳定,您将更欣赏单元测试,但是即使现在您的测试也突出了对框架更改的传播程度。

这是值得的,尽您所能坚持下去。


我同意ck的观点,就我在多家公司中进行单元测试集成所见,单元测试框架更常用于功能测试而非单元测试。
尼古拉斯,2009年

几个月前,我写了一个有关该主题的博客:cwash.org/2009/02/17/dont-unit-test-anymore-no-really
cwash

也就是说,如何将“开发人员测试”术语与“单元测试”术语以及混淆原因的含义相混淆...
cwash

12

没有更多信息,很难知道您为什么遭受这些问题。有时不可避免的是,更改接口等会破坏很多事情,而其他时候则是设计问题。

尝试对遇到的故障进行分类是一个好主意。你有什么问题?例如,它是由于API更改而进行测试维护(如使其在重构后进行编译!),还是归因于API更改的行为?如果可以看到模式,则可以尝试更改生产代码的设计,或者更好地隔离测试以免更改。

如果在许多地方进行一些更改会给测试套件带来难以估量的破坏,那么您可以做一些事情(大多数只是常见的单元测试技巧):

  • 开发小代码单元并测试小代码单元。提取有意义的接口或基类,以便代码单元中包含“接缝”。您需要引入的依赖关系越多(或更糟的是,使用“ new”在类内部实例化),更改代码的可能性就越大。如果每个代码单元都具有少数依赖项(有时是一对或根本没有),那么最好避免更改。

  • 只能断言测试需要什么。不要断言中间,偶然或无关的状态。按合同设计和按合同测试(例如,如果要测试堆栈弹出方法,则不要在推送后测试count属性-应该在单独的测试中)。

    我看到了这个问题,尤其是在每个测试都是一个变体的情况下。如果任何偶然状态发生变化,它将破坏声明在其上的所有内容(无论是否需要声明)。

  • 与普通代码一样,在单元测试中使用工厂和构建器。我了解到,在API更改后,大约有40个测试需要更新构造函数调用时...

  • 同样重要的是,请先使用前门。如果可用,您的测试应始终使用正常状态。仅在需要时(即没有要验证的状态)使用基于交互的测试。

无论如何,要点是我将尝试找出测试为何/在何处中断并从那里进行。尽最大努力使自己与变化隔离。


8

单元测试的好处之一是,当您进行这样的更改时,可以证明您没有破坏代码。您的确必须使测试与框架保持同步,但是,这项相当平凡的工作比尝试找出重构时发生的故障要容易得多。


4

我会坚持要求您坚持TDD。尝试检查您的单元测试框架,与您的团队进行一次RCA(根本原因分析),并确定区域。

将单元测试代码固定在套件级别,不要经常更改代码,尤其是功能名称或其他模块。

如果您能很好地分享您的案例研究,我们将在问题领域进行更多的挖掘,将不胜感激?


4

好问题!

设计好的单元测试与设计软件本身一样困难。开发人员很少意识到这一点,因此结果通常是草率编写的单元测试,每当被测系统发生变化时,都需要维护。因此,解决您的问题的部分方法可能是花费更多的时间来改进单元测试的设计。

我可以推荐一本值得一提的出色书籍,即《单元测试的设计模式》。

高温超导


绝对是一本好书,很可能解决了提问者遇到的问题
弗兰克·施维特曼

1
很多书籍可以帮助您开始使用TDD和单元测试。上面的书在您遇到困难时可以提供帮助。
皮特·特玛

确实。如果卡住了,只需在正确的页面上将其打开!
azheglov

4

如果问题是您的测试与实际代码已过时,则可以执行以下一项或两项操作:

  1. 训练所有开发人员不要通过不会更新单元测试的代码审查。
  2. 设置一个自动测试框,在每次签入后运行完整的单元测试,并通过电子邮件将中断构建的人员发送给您。(我们曾经以为这只是针对“大男孩”的,但是我们在专用盒子上使用了开源软件包。)

3

好吧,如果代码中的逻辑已更改,并且您已经为这些代码段编写了测试,那么我认为需要更改测试以检查新的逻辑。单元测试应该是测试代码逻辑的相当简单的代码。


3

您的单元测试正在执行应做的事情。揭露由于框架,即时代码或其他外部源的更改而导致的行为中断。这样做的目的是帮助您确定行为是否确实发生了变化,是否需要相应地修改单元测试,或者是否引入了错误从而导致单元测试失败并需要更正。

不要放弃,尽管它现在令人沮丧,但其好处将会实现。


2

我不确定使您的代码难以维护测试的具体问题,但是当我遇到类似的问题而导致测试中断时,我可以分享一些自己的经验。我最终了解到,缺乏可测试性主要是由于被测类的一些设计问题:

  • 使用具体的类而不是接口
  • 使用单例
  • 调用大量用于业务逻辑和数据访问的静态方法,而不是接口方法

因此,我发现通常我的测试正在中断-不是由于被测类的更改-而是由于被测类正在调用的其他类的更改。通常,重构类以请求其数据依赖性并使用模拟对象进行测试(EasyMock等针对Java)使测试更加集中和可维护。我真的很喜欢一些有关此主题的网站:


+1 Google的东西真的很好。googletesting.blogspot.com/2008/11/…对于那些不喜欢阅读太多内容的人来说是一个很好的展示;-)
jens

2

为什么每次更改框架时都必须更改单元测试?难道不是相反吗?

如果您使用的是TDD,则应首先确定测试正在测试错误的行为,而应改为验证是否存在所需的行为。现在,您已经修复了测试,测试将失败,并且必须消除框架中的错误,直到测试再次通过。


1

当然,一切都附带价格。在开发的早期阶段,通常必须更改许多单元测试。

您可能想查看一下代码中的一些内容,以进行更多的封装,创建更少的依赖关系等。

当您临近生产日期时,您会很高兴拥有这些测试,请相信我:)


我们目前处于发布周期中,这是一个问题,因为当我们需要修复网站接受错误时,我们无暇花费时间来修复测试。
Burt

2
@Burt这没有任何意义。您首先编写不想更改的行为的单元测试。如果您希望行为每隔一天改变一次,请不要编写单元测试。有点奇怪的是写它们,然后抱怨它们失败。仅当您想要具有稳定,可重复行为的代码时,才需要单元测试。如果您希望软件不断更改功能,请不要对其进行单元测试。但是,在这种情况下,请进行手动测试,祝您好运。
丹尼尔·达拉纳斯

我们如何在不更改框架的情况下发展框架?它是基础结构代码,我在城堡堆栈上使用了一段时间,处理了主干代码,并且更改了很多,所以我认为在确定新需求时通过重构和更改来改进框架是很常见的。
Burt

(A)您自动进行测试,然后在行为更改时必须修改测试;(B)您用Word文档或对行为的口碑期望手动测试它,然后在测试它时,您必须告诉测试者“嗨,乔,从现在开始,当您执行X和Y时,Z必须而不是W“,或者(C)您不对其进行测试。看起来您正在(A)和(C)之间进行计算的某个中间层。两者都有成本和优势。
丹尼尔·达拉纳斯

1

您的单元测试不是面向黑盒的吗?我的意思是...让我举个例子:假设您正在对某种容器进行单元测试,是否使用容器的get()方法来验证新项是否已实际存储,或者您是否设法获取了实际的存储库可以直接在存储位置检索项目?后者进行脆弱的测试:更改实现时,您正在破坏测试。

您应该针对接口而不是内部实现进行测试。

而且,当您更改框架时,最好先尝试更改测试,然后再更改框架。


1

我建议您投资测试自动化工具。如果您使用的是持续集成,则可以使其协同工作。有一些工具可以扫描您的代码库并为您生成测试。然后将运行它们。这种方法的缺点是它太通用了。因为在许多情况下,单元测试的目的是破坏系统。我编写了许多测试,是的,如果代码库发生更改,我必须更改它们。

自动化工具有一条很好的界线,您肯定会更好地覆盖代码。

但是,使用基于wrttien develper的良好测试,您还将测试系统完整性。

希望这可以帮助。


1

如果您的代码真的很难测试,并且测试代码中断或需要付出很大的努力才能保持同步,那么您会遇到更大的问题。

考虑使用提取方法重构抽取出只做一件事而又做一件事的小代码块;没有依赖关系,并将您的测试写入那些小的方法。


1

额外的代码似乎已经成为维护工作的头疼问题,因为当内部框架发生更改时,我们必须四处解决并修复所有与之相关的单元测试。

另一种选择是,当您的Framework更改时,您无需测试更改。或者您根本不测试框架。那是你要的吗?

您可以尝试重构您的框架,使其由可以独立测试的较小部分组成。然后,当您的Framework发生更改时,您希望(a)更改的部分更少或(b)更改的主要部分是组成这些部分的方式。两种方式都可以使您更好地重用代码和测试。但是涉及真正的智力努力。不要指望那么容易。


理想情况下,我们的框架应该是接口驱动的,这意味着我们可以对其进行大量模拟,但是可以确定继承是更好的选择,正是这种继承导致了问题,并使功能难以测试。
Burt 2010年

1

我发现,除非您使用鼓励鼓励编写很小的类的IoC / DI方法,并且认真地遵循“单一职责原则”,否则单元测试最终会测试多个类的交互,这会使它们非常复杂,因此很脆弱。

我的观点是,许多新颖的软件开发技术只能在一起使用时才能起作用。特别是MVC,ORM,IoC,单元测试和模拟。DDD(在现代原始意义上)和TDD / BDD更加独立,因此您可以使用或不使用它们。


0

有时,设计TDD测试会引发对应用程序本身设计的质疑。检查您的类是否设计得很好,您的方法一次只执行一件事。如果设计得好,编写代码来测试简单的方法和类应该很简单。


0

我自己一直在考虑这个话题。我对单元测试的价值非常满意,但对严格的TDD却不满意。在我看来,在某种程度上,您可能正在进行探索性编程,在这种情况下,您需要将事物划分为类/接口的方式进行更改。如果您在旧类结构的单元测试上投入了大量时间,那么重构的惯性就会增加,很难舍弃这些额外的代码,等等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.