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

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

要最大化代码的可testing性,请遵循以下规则:

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

  2. 使用dependency injectiondevise类。 原因:你不能模拟或testing什么是看不到的。

  3. 使用Model-View-Controller或Model-View-Presenter将UI代码与其行为分开。 原因:允许testing业务逻辑,而不能testing的部分(UI)被最小化。

  4. 不要写静态方法或类。 原因:静态方法很难或不可能被隔离,Rhino Mock无法嘲笑它们。

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

  6. 隔离外部依赖。 原因:无法testing未parsing的外部依赖关系。

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

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

先写testing,然后写代码。

我同意,高层次。 但是,我会更具体地说:“先写testing,然后写足够的代码来通过testing,然后重复。” 否则,我恐怕我的unit testing看起来更像集成或验收testing。

使用dependency injectiondevise类。

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

使用Model-View-Controller或Model-View-Presenter将UI代码与其行为分开。

同意。 请注意,即使演示者/控制器也可以使用DI / IoC进行testing,方法是将模拟视图和模拟视图模型化。 查看Presenter First TDD了解更多信息。

不要写静态方法或类。

不知道我同意这个。 可以unit testing静态方法/类而不使用模拟。 所以,也许这是你提到的Rhino Mock特定规则之一。

编程接口,而不是类。

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

隔离外部依赖。

同意。 使用接口隐藏外部依赖关系(适用时)。 这将允许您将您的软件与外部依赖关系隔离,无论是Web服务,队列,数据库还是其他内容。 当你的团队不控制依赖关系(又名外部)时,这一点尤为重要。

将虚拟的方法标记为虚拟的方法。

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

而且,还有几点需要考虑:

使用创作devise模式。 这将有助于DI,但它也允许您隔离该代码并独立于其他逻辑进行testing。

用Bill Wake的Arrange / Act / Assert技术写testing。 这种技术非常清楚什么configuration是必要的,什么是实际testing,以及预期的。

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

避免将unit testing中的重复重构为抽象基类或者设置/拆卸方法的诱惑。 这样做会隐藏开发人员的configuration/清理代码,以便进行unit testing。 在这种情况下,每个单独testing的清晰度比重构重要性更重要。

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

如果您正在使用.Net 3.5,您可能需要查看Moq嘲讽库 – 它使用expression式树和lambdaexpression式去除大多数其他嘲讽库的非直观的logging – 回复成语。

看看这个快速入门 ,看看你的testing用例变得更直观了,这里是一个简单的例子:

// 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)); 

知道假货,嘲笑和存根之间的区别,以及何时使用每个。

避免使用模拟指定交互。 这使得testing变得脆弱 。

这是一个非常有用的职位!

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

对于一些生活在完全学术领域的人来说,时间表和交付可能并不重要,但是在软件就是金钱的环境中,有效利用TDD的努力是至关重要的。

TDD高度服从边际收益递减规律。 简而言之,你在TDD方面的努力越来越有价值,直到你达到最高回报率,之后,投入TDD的时间越来越less。

我倾向于认为,TDD的主要价值在于边界(黑匣子)以及偶尔对系统任务关键区域进行白盒testing。

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

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

哦,只是想到了你可能想要做的事情。 我通常有一个unit testing库,每个类testing都有一个testing装置,每个testing进入一个相应的命名空间。 我也倾向于有更多的BDD风格的另一个testing库(集成testing?)。 这使我可以编写testing来说明这个方法应该做什么以及应用程序应该做什么。

这是另一个我认为我喜欢做的事情。

如果你打算从unit testingGui运行testing,而不是从TestDriven.Net或NAnt运行,那么我发现将unit testing项目types设置为控制台应用程序而不是库是比较容易的。 这使您可以手动运行testing,并以debugging模式(上述的TestDriven.Net实际上可以为您执行)进行testing。

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