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

时间:2020-03-06 14:37:51  来源:igfitidea点击:

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

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

  • 首先编写测试,然后编写代码。原因:这样可以确保我们编写可测试的代码,并确保每一行代码都可以为其编写测试。
  • 使用依赖注入设计类。原因:我们不能嘲笑或者测试看不见的东西。
  • 使用Model-View-Controller或者Model-View-Presenter将UI代码与其行为分开。原因:在最小化无法测试的部分(UI)的同时,允许测试业务逻辑。
  • 不要编写静态方法或者类。原因:静态方法很难或者不可能隔离,Rhino Mocks无法模拟它们。
  • 编程关闭接口,而不是类。原因:使用接口可以阐明对象之间的关系。接口应定义对象从其环境中需要的服务。另外,可以使用Rhino Mocks和其他模拟框架轻松模拟接口。
  • 隔离外部依赖项。原因:无法测试未解决的外部依赖关系。
  • 将要模拟的方法标记为虚拟。原因:Rhino Mocks无法模拟非虚拟方法。

解决方案

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

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

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

Write the test first, then the code.

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

Design classes using dependency injection.

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

Separate UI code from its behavior using Model-View-Controller or Model-View-Presenter.

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

Do not write static methods or classes.

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

Program off interfaces, not classes.

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

Isolate external dependencies.

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

Mark as virtual the methods you intend to mock.

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

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

使用创新的设计模式。这将对DI有所帮助,但是它还允许我们隔离该代码并独立于其他逻辑对其进行测试。

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

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

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

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

了解假货,假货和存根之间的区别以及何时使用它们。

避免使用模拟过度指定交互。这会使测试变脆。

如果使用的是.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));

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

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

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

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

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

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

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

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

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