如何编写易于重构的单元测试TDD测试的技巧

13

我已经在一个ASP.NET MVC项目上工作了大约8个月。大部分时间我都在使用TDD,但有些方面的单元测试只是在我编写实际代码之后才覆盖了它们。总体而言,该项目的测试覆盖率相当好。

到目前为止,我对结果感到非常满意。重构确实更容易了,我的测试也帮助我在第一次运行软件之前发现了很多错误。此外,我还开发了更复杂的伪造对象和辅助程序来帮助我最小化测试代码。

然而,我真正不喜欢的是,我经常发现自己不得不更新现有的单元测试以适应我对软件所做的重构。重构软件现在很快,也很容易,但重构我的单元测试则相当无聊和繁琐。事实上,维护我的单元测试的成本比起一开始编写它们来说更高。

我想知道是否我做错了什么?或者测试开发成本与测试维护成本的这种关系是否正常?我已经尝试编写尽可能多的测试用例,以便这些测试用例覆盖我的用户故事,而不是像这篇博客文章中建议的那样系统地覆盖我的对象接口。

此外,您有什么进一步的提示,可以帮助我编写TDD测试用例,使重构对测试的破坏尽可能少?

编辑:正如Henning和tvanfosson正确指出的那样,通常是设置部分最难编写和维护的。破碎的测试通常是由于对这些测试的设置部分不兼容的域模型重构导致的(根据我的经验)。


1
你能举一个具体的例子,说明重构代码如何影响单元测试吗?虽然有时会发生这种情况,但我个人认为真正的“单元”测试不应受到重构的影响(除了移动一些测试和/或为重构后的代码编写新的测试)。我知道这不是一个非常有帮助的答案,希望具体的例子能够得出一个更好的答案。 - kdgregory
7个回答

8
这是一个众所周知的问题,可以通过按照最佳实践编写测试来解决。这些实践在优秀的xUnit Test Patterns中有所描述。该书描述了导致难以维护的测试的测试气味,并提供了关于如何编写可维护单元测试的指导。
在长期遵循这些模式之后,我编写了AutoFixture,这是一个封装了许多核心模式的开源库。
它作为Test Data Builder工作,但也可以被连接起来作为自动模拟容器并做许多其他奇妙的事情。
它对于维护非常有帮助,因为它显著提高了编写测试的抽象级别。测试变得更加声明性,因为您可以声明您想要某种类型的实例,而不是明确地编写如何创建它。
想象一下,您有一个带有此构造函数签名的类
public MyClass(Foo foo, Bar bar, Sgryt sgryt)

只要AutoFixture可以解析所有构造函数参数,你就可以像这样简单地创建一个新实例:
var sut = fixture.CreateAnonymous<MyClass>();

主要好处是,如果您决定重构MyClass构造函数,没有测试会失败,因为AutoFixture会为您解决这个问题。
这只是AutoFixture的一小部分功能。它是一个独立的库,因此可以与您选择的单元测试框架一起使用。

非常感谢,我会看一下! - Adrian Grigore
1
@Mark Seemann感谢您的回答。提到xUnit测试模式书对我特别有帮助。它似乎正是我正在寻找的:我一定会订购一本。 - phuibers

2
您可能会把编写单元测试的重点放在类上。您应该测试公共API。所谓公共API,并不是指所有类上的公共方法,而是指公共控制器。
通过让测试模仿用户与控制器部分交互的方式,而不直接触及模型类或辅助函数,您可以在不必重构测试的情况下重构代码。当然,有时候即使您的公共API发生变化,您仍然需要更改测试,但这种情况会少得多。
这种方法的缺点是,您经常需要经过复杂的控制器设置才能测试您想引入的新小型辅助函数,但我认为最终它是值得的。此外,您将以更智能的方式组织测试代码,使设置代码更易于编写。

那么你一定做错了什么,因为在重构代码时,你不应该经常重构测试。 - Virgil Dupras
我没有说是持续不断的。但我确实发现TDD测试创建的成本超过了维护成本。根据你的经验,这个情况有所不同吗? - Adrian Grigore
在我的经验中,情况并非如此。我很少需要重构我的测试,因为我的公共控制器的工作方式很少改变。当我添加新功能时,它通常不会影响其余代码。当我必须重构东西时,通常是在我的公共控制器API的保护下进行的。 - Virgil Dupras

2

谢谢这篇文章,我会看一下。当然,我知道所有事情都是有代价的。我想知道为什么人们在讨论TDD是否真的有意义时抱怨单元测试写起来太繁琐,而我花费的时间更多的是维护它们而不是编写它们。 - Adrian Grigore

1

我认为他的意思是设置部分相当繁琐难以维护。 我们也遇到了完全相同的问题,特别是在引入新的依赖项、拆分依赖项或者改变代码使用方式时。

大多数情况下,当我编写和维护单元测试时,我花费时间在编写设置/安排代码上。 在我们的许多测试中,我们有完全相同的设置代码,并且有时使用私有帮助方法来执行实际设置,但使用不同的值。

然而,这并不是一个真正好的事情,因为我们仍然必须在每个测试中创建所有这些值。因此,我们现在正在研究以更规范/BDD风格编写测试,这应该有助于减少设置代码,从而减少维护测试所需的时间。 你可以查看一些资源,比如http://elegantcode.com/2009/12/22/specifications/,以及使用MSpec进行BDD风格测试http://elegantcode.com/2009/07/05/mspec-take-2/


1
大多数时候,我看到这样的重构会影响单元测试的设置,通常涉及添加依赖项或更改对这些依赖项的期望。这些依赖关系可能是由后续功能引入的,但却影响了之前的测试。在这些情况下,我发现重构设置代码非常有用,使其可以被多个测试共享(参数化以便可以灵活配置)。然后,当我需要为影响设置的新功能进行更改时,我只需要在一个地方重构测试即可。

1
我也曾通过添加虚假对象工厂来实现类似的功能。我还有一个基础测试类,负责依赖注入。有时我也会有一个设置方法被多个测试共享。我想这就是你所说的重构设置代码的意思吧? - Adrian Grigore

0
如果你发现自己正在创建涉及深层对象图像俄罗斯套娃的复杂测试脚手架,请考虑重构你的代码,使得被测试的类在其构造函数/参数中获得所需的内容,而不是让它遍历整个图像。
与其这样做:
public class A {

   public void foo(B b) {
      String someField = b.getC().getD().getSomeField();
      // ...
   }
} 

将其更改为:

public class A {

   public void foo(String someField) {
      // ...
   }
} 

那么你的测试设置就变得非常简单。


0

当我开始感到重构设置痛苦时,我专注于两个方面:使我的单元测试更具体和我的方法/类更小。本质上,我发现我正在远离SOLID / SRP。或者我有一些试图做太多事情的测试。

值得注意的是,我尽量远离BDD /上下文规范,因为我离UI越远,测试行为就越好,但总是导致我(也许我做错了?)有更多上下文规范的混乱测试。

另一种我看到这种情况发生在我的代码债务中,随着时间的推移,业务逻辑不断增长。当然,总会有具有多个依赖项的大型方法和类,但我拥有的越少,我需要“重写测试”的次数就越少。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接