使用C#和RhinoMocks进行测试驱动开发的最佳实践


86

为了帮助我的团队编写可测试的代码,我想出了这个简单的让我们的C#代码库更具可测试性的最佳实践列表。 (其中一些观点指的是Rhino Mocks的限制,这是C#的嘲讽框架,但规则也可能更普遍适用。)有没有人有他们遵循的最佳实践?

为了最大限度地提高代码的可测试性,遵循下列规则:

  1. 先写测试,然后代码。原因:这可确保您编写可测试的代码,并确保每行代码都为其编写测试。

  2. 使用依赖注入的设计类。原因:你不能模拟或测试无法看到的东西。

  3. 使用Model-View-Controller或Model-View-Presenter从其行为中分离出UI代码。原因:允许在无法测试的部分(UI)最小化时测试业务逻辑。

  4. 不要编写静态方法或类。原因:静态方法很难或无法分离,Rhino Mocks无法嘲笑它们。

  5. 编程接口,而不是类。原因:使用接口可以阐明对象之间的关系。一个接口应该定义一个对象在其环境中需要的服务。另外,使用Rhino Mocks和其他嘲讽框架可以轻松模拟界面。

  6. 隔离外部依赖关系。原因:无法测试未解析的外部依赖关系。

  7. 将虚拟标记为您打算模拟的方法。原因:Rhino Mocks无法模拟非虚拟方法。

  0

这是一个有用的列表。我们目前正在使用NUnit和Rhino.Mocks,对于不太熟悉单元测试这一方面的团队成员来说,明确这些标准是很好的。 24 9月. 082008-09-24 13:38:02

56

绝对是一个很好的清单。这里有几点想法:

先写测试,然后是代码。

我同意,在高层次。但是,我会更具体一些:“先写一个测试,然后写出只需要代码来通过测试,然后重复。”否则,我会担心我的单元测试看起来更像集成或验收测试。

使用依赖注入的设计类。

同意。当一个对象创建它自己的依赖关系时,你无法控制它们。反转控制/依赖注入为您提供了该控件,允许您使用mocks/stubs /等隔离被测对象。这就是你如何独立地测试对象。

使用Model-View-Controller或Model-View-Presenter从其行为中分离出UI代码。

同意。请注意,即使是演示者/控制器也可以使用DI/IoC进行测试,方法是将其呈现为残留/模拟视图和模型。查看Presenter First TDD了解更多。

不要编写静态方法或类。

不确定我是否同意这一点。可以在不使用mock的情况下对静态方法/类进行单元测试。所以,也许这是你提到的那些Rhino Mock特定规则之一。

编程接口,而不是类。

我同意,但原因稍有不同。接口为软件开发人员提供了极大的灵活性 - 除了支持各种模拟对象框架之外。例如,无法在没有接口的情况下正确支持DI。

隔离外部依赖关系。

同意。使用界面隐藏外部依赖关系(如适用)。这将允许您将软件与外部依赖关系隔离,无论是Web服务,队列,数据库还是其他内容。这是,尤其是当你的团队不控制依赖(a.k.a. external)时很重要。

标记为您想要模拟的方法。

这是Rhino Mocks的局限性。在一个喜欢手工编码存根的模拟对象框架环境中,这不是必需的。

而且,一对夫妇的新需要考虑的要点:

使用造物设计模式。这将有助于DI,但它也允许您隔离该代码并独立于其他逻辑进行测试。

使用Bill Wake's Arrange/Act/Assert technique编写测试。这项技术非常清楚需要什么配置,实际测试的内容以及期望的内容。

不要害怕推出自己的嘲笑/存根。通常,您会发现使用模拟对象框架会让您的测试难以理解。通过滚动你自己,你可以完全控制你的模拟/存根,并且你将能够保持你的测试可读性。 (请参阅前一点。)

避免将单元测试中的重复重构为抽象基类或设置/拆卸方法的诱惑。这样做会隐藏来自开发人员的配置/清理代码,以尝试单元测试。在这种情况下,每个单独测试的清晰度比重构重复更重要。

实施持续集成。在每个“绿色栏”上签入您的代码。构建您的软件并在每次签到时运行全套单元测试。 (当然,这本身并不是一种编码习惯,但它是保持软件清洁和完全集成的一个令人难以置信的工具。)

+2

我通常会发现,如果测试很难读取,它不是框架的错误,而是它正在测试的代码的错误。如果SUT设置起来很复杂,那么也许它应该被分解成更多的概念。 06 9月. 092009-09-06 19:42:58


1

好的清单。你可能想要建立的东西之一 - 我不能给你太多的建议,因为我自己开始考虑 - 当一个类应该位于不同的库,名称空间,嵌套的命名空间中时。您甚至可能希望事先弄清楚库和名称空间的列表,并要求团队必须见面并决定合并两个/添加一个新的。

哦,只是想到我做的事情,你可能也想。我通常有一个单元测试库,每个类测试都有一个测试装置,每个测试进入一个相应的命名空间。我还倾向于有另一个测试库(集成测试?),它更多地位于BDD style。这使我可以编写测试来指出该方法应该做什么以及应用程序应该做什么。

  0

我也在个人项目中做了类似的BDD风格测试部分(除了单元测试代码)。 24 9月. 082008-09-24 00:11:03


6

了解fakes, mocks and stubs和何时使用它们之间的区别。

避免使用模拟指定交互。这使得测试brittle


10

如果您正在使用.Net 3.5,您可能需要查看Moq模拟库 - 它使用表达式树和lambda表达式去除大多数其他模拟库的非直观的记录 - 回复成语。

看看这个quickstart看到测试用例的更加直观而成,这里是一个简单的例子:

// ShouldExpectMethodCallWithVariable 
int value = 5; 
var mock = new Mock<IFoo>(); 

mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2); 

Assert.AreEqual(value * 2, mock.Object.Duplicate(value)); 
+5

我认为Rhino Mocks的新版本也可以这样工作 24 9月. 082008-09-24 13:21:06


0

这里有一个另外一个,我想到的是我喜欢做的事。

如果计划运行从TestDriven.Net或恶性不是从单元测试GUI测试,然后我发现它更容易设置的单元测试项目类型的控制台应用程序,而不是库。这使您可以手动运行测试并在调试模式下执行测试(前面提到的TestDriven.Net实际上可以为您执行测试)。

另外,我总是喜欢开放一个Playground项目来测试我不熟悉的代码和想法。这不应该检查到源代码管理。更好的是,它应该只在开发人员的机器上的单独的源代码控制库中。


3

这是一个非常有帮助的职位!

我想补充一点,理解上下文和被测系统(SUT)总是很重要的。当您在现有代码遵循相同主体的环境中编写新代码时,TDD主体对信件的处理要容易得多。但是,当您在非TDD传统环境中编写新代码时,您发现TDD的努力可能会迅速超出您的估计和期望。

对于你们中的一些,谁住在一个完全学术界,时限和交付也许并不重要,但在软件就是金钱,有效地利用你的TDD工作的环境是至关重要的。

TDD高度服从于Diminishing Marginal Return的法律。简而言之,您在TDD方面的努力越来越有价值,直到您达到最高回报率,之后,随后投入TDD的时间越来越少。

我倾向于认为TDD的主要价值在于边界(黑匣子)以及临时白盒测试系统的任务关键区域。


2

接口编程的真正原因并不是为了让Rhino的生活更轻松,而是为了阐明代码中对象之间的关系。一个接口应该定义一个对象在其环境中需要的服务。一个类提供了该服务的特定实现。阅读Rebecca Wirfs-Brock关于角色,责任和合作者的“对象设计”一书。

  0

同意...我将更新我的问题以反映这一点。 09 9月. 092009-09-09 23:29:56