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


86

为了帮助我的团队编写可测试的代码,我提出了一些简单的最佳实践,以使我们的C#代码库更可测试。(有些观点涉及C#模拟框架Rhino Mocks的局限性,但这些规则也可能更普遍地适用。)是否有人遵循任何最佳实践?

为了最大化代码的可测试性,请遵循以下规则:

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

  2. 使用依赖注入设计类。原因:您不能嘲笑或测试看不见的东西。

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

  4. 不要编写静态方法或类。 原因:静态方法很难或不可能隔离,Rhino Mocks无法模拟它们。

  5. 编程关闭接口,而不是类。原因:使用接口可以阐明对象之间的关系。接口应定义对象从其环境中需要的服务。同样,可以使用Rhino Mocks和其他模拟框架轻松模拟接口。

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

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


这是一个有用的列表。我们目前正在使用NUnit和Rhino.Mocks,对于不熟悉单元测试这一方面的团队成员,最好列出这些标准。
克里斯·巴拉德

Answers:


58

绝对是一个不错的清单。这里有一些想法:

首先编写测试,然后编写代码。

我同意,在较高的层次上。但是,我会更具体:“先编写一个测试,然后编写足够的代码以通过测试,然后重复执行。” 否则,我会担心我的单元测试看起来更像是集成测试或验收测试。

使用依赖注入设计类。

同意 当对象创建自己的依赖项时,您将无法对其进行控制。控制反转/依赖注入为您提供了控制权,使您可以通过模拟/存根/等隔离测试对象。这就是您隔离测试对象的方式。

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

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

不要编写静态方法或类。

不确定我是否同意这一点。无需使用模拟就可以对静态方法/类进行单元测试。因此,也许这是您提到的Rhino Mock特定规则之一。

编程关闭接口,而不是类。

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

隔离外部依赖项。

同意 通过接口将外部依赖项隐藏在自己的外观或适配器(根据需要)之后。这将使您可以将软件与外部依赖项隔离开来,无论它是Web服务,队列,数据库还是其他东西。当您的团队无法控制依赖关系(又称外部)时,这一点尤其重要。

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

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

并且,有几点需要考虑的新问题:

使用创新的设计模式。这将有助于DI,但也可以使您隔离该代码并独立于其他逻辑对其进行测试。

使用Bill Wake的Arrange / Act / Assert技术编写测试。这项技术非常清楚地表明需要什么配置,实际上正在测试什么以及期望什么。

不要害怕推出自己的模拟/存根。通常,您会发现使用模拟对象框架使测试难以置信。通过自己动手,您可以完全控制自己的模拟/存根,并且可以保持测试的可读性。(请参考上一点。)

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

实施持续集成。在每个“绿色栏”上签入您的代码。构建您的软件,并在每次签入时运行全套的单元测试。(当然,这本身不是编码实践;但这是使软件保持干净和完全集成的不可思议的工具。)


3
我通常会发现,如果很难阅读测试,这不是框架的错,而是测试的代码。如果SUT的设置很复杂,那么也许应该将其分解为更多概念。
史蒂夫·弗里曼

10

如果使用的是.Net 3.5,则可能需要研究Moq模拟库-它使用表达式树和lambda来删除大多数其他模拟库的非直观的记录回复惯用法。

查看此快速入门,看看您的测试用例变得更加直观,这是一个简单的示例:

// 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
我认为,犀牛制品的新版本是这样工作太
乔治·莫尔


3

这是一个非常有帮助的帖子!

我要补充一点,了解上下文和被测系统(SUT)始终很重要。在现有代码遵循相同主体的环境中编写新代码时,在字母上遵循TDD主体要容易得多。但是,当您在非TDD旧式环境中编写新代码时,您会发现您的TDD工作可能迅速膨胀,远远超出了您的估计和期望。

对于生活在整个学术世界中的某些人来说,时间安排和交付可能并不重要,但是在软件就是金钱的环境中,有效利用TDD的工作至关重要。

TDD严格遵守边际收益递减法则。简而言之,您在TDD上所做的努力越来越有价值,直到您达到最大回报点,此后,对TDD进行后续投资的价值越来越小。

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


2

针对接口进行编程的真正原因不是使Rhino的工作更轻松,而是阐明代码中对象之间的关系。接口应定义对象从其环境中需要的服务。类提供了该服务的特定实现。阅读Rebecca Wirfs-Brock的“对象设计”一书,内容涉及角色,职责和协作者。


同意...我将更新我的问题以反映这一点。
凯文·阿尔布雷希特(

1

好清单。您可能想建立的一件事-当我应该开始自己考虑时,就不能给您太多建议了-当一个类应位于不同的库,名称空间,嵌套名称空间中时。您甚至可能需要事先找出库和名称空间的列表,并要求团队必须开会并决定合并两个/添加一个新的库和命名空间。

哦,只是想到了我可能想要做的一些事情。我通常有一个基于类策略的测试夹具的单元测试库,其中每个测试都进入相应的命名空间。我也倾向于使用另一个BDD风格的测试库(集成测试?)。这使我可以编写测试来确定该方法应该做什么以及应用程序应该做什么。


我还在个人项目中做了类似的BDD样式测试部分(除了单元测试代码之外)。
凯文·阿尔布雷希特

0

这是我想到的另一个我喜欢做的事情。

如果您打算从单元测试Gui运行测试,而不是从TestDriven.Net或NAnt运行测试,那么我发现将单元测试项目类型设置为控制台应用程序而不是库更容易。这使您可以手动运行测试,并在调试模式下逐步进行测试(上述TestDriven.Net实际上可以为您完成)。

另外,我总是喜欢打开一个Playground项目,以测试我不熟悉的一些代码和想法。不应将此检查到源代码管理中。更好的是,它应该仅位于开发人员计算机上的单独的源代码控制存储库中。

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.