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

为了帮助我的团队编写可测试的代码,我提出了这个简单的最佳实践列表,以使我们的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无法模拟非虚方法。

Kevin Albrecht asked 2019-09-11T04:01:08Z
7个解决方案
58 votes

绝对是一个很好的清单。 以下是一些想法:

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

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

使用依赖注入设计类。

同意。 当对象创建自己的依赖项时,您无法控制它们。 控制/依赖注入的反转为您提供了控制,允许您使用模拟/存根/等隔离测试对象。 这是您单独测试对象的方法。

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

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

不要编写静态方法或类。

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

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

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

隔离外部依赖项。

同意。 使用接口隐藏您自己的外观或适配器(视情况而定)后面的外部依赖项。 这将允许您将软件与外部依赖关系隔离,无论是Web服务,队列,数据库还是其他内容。 当您的团队无法控制依赖关系时(a.k.a. external),这一点尤为重要。

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

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

并且,需要考虑几个新点:

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

使用Bill Wake的Arrange / Act / Assert技术编写测试。 这种技术非常清楚地说明了必要的配置,实际测试的内容以及预期的内容。

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

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

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

aridlehoover answered 2019-09-11T04:03:37Z
10 votes

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

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

// 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));
zadam answered 2019-09-11T04:04:09Z
6 votes

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

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

Hamish Smith answered 2019-09-11T04:04:38Z
3 votes

这是一个非常有用的帖子!

我想补充一点,理解上下文和被测系统(SUT)总是很重要的。 当您在现有代码遵循相同主体的环境中编写新代码时,遵循TDD原则会更容易。 但是,当您在非TDD遗留环境中编写新代码时,您会发现您的TDD工作可能会迅速超出您的预期和期望。

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

TDD受到边际收益递减法的高度制约。 简而言之,在达到最大回报之前,您对TDD的努力越来越有价值,之后,投入TDD的后续时间价值越来越低。

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

answered 2019-09-11T04:05:29Z
2 votes

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

Steve Freeman answered 2019-09-11T04:05:53Z
1 votes

好清单。 你可能想要建立的一件事 - 我不能给你很多建议,因为我自己刚刚开始考虑它 - 是一个类应该在不同的库,命名空间,嵌套命名空间中。 您甚至可能希望事先确定一个库和命名空间列表,并要求团队必须满足并决定合并两个/添加一个新的。

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

George Mauer answered 2019-09-11T04:06:25Z
0 votes

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

如果您计划从单元测试Gui而不是TestDriven.Net或NAnt运行测试,那么我发现将单元测试项目类型设置为控制台应用程序而不是库更容易。 这允许您手动运行测试并在调试模式中逐步执行它们(前面提到的TestDriven.Net实际上可以为您完成)。

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

George Mauer answered 2019-09-11T04:07:02Z
translate from https://stackoverflow.com:/questions/124210/best-practices-of-test-driven-development-using-c-sharp-and-rhinomocks