88

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

4

7 回答 7

59

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

先写测试,再写代码。

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

使用依赖注入设计类。

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

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

同意。请注意,即使是演示者/控制器也可以使用 DI/IoC 进行测试,方法是为其提供一个 stubbed/mocked 视图和模型。查看Presenter First TDD 了解更多信息。

不要编写静态方法或类。

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

对接口进行编程,而不是类。

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

隔离外部依赖。

同意。使用接口隐藏您自己的外观或适配器(视情况而定)后面的外部依赖项。这将允许您将您的软件与外部依赖项隔离开来,无论是 Web 服务、队列、数据库还是其他东西。当您的团队不控制依赖项(也称为外部)时,这一点尤其重要。

将您打算模拟的方法标记为虚拟。

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

而且,有几点需要考虑:

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

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

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

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

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

于 2008-09-24T05:32:30.427 回答
10

如果您正在使用 .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));
于 2008-09-24T05:44:57.337 回答
6

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

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

于 2008-09-24T05:42:26.207 回答
4

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

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

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

TDD 高度服从边际收益递减法则。简而言之,在达到最大回报点之前,您在 TDD 上所做的努力会变得越来越有价值,之后投入到 TDD 上的后续时间的价值就会越来越少。

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

于 2009-05-06T02:23:23.750 回答
2

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

于 2009-09-06T19:45:38.720 回答
1

好清单。您可能想要确定的一件事——我不能给你太多建议,因为我自己刚刚开始考虑它——是什么时候一个类应该在不同的库、命名空间、嵌套命名空间中。您甚至可能想事先找出一个库和命名空间列表,并要求团队必须开会并决定合并两个/添加一个新的。

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

于 2008-09-24T00:06:29.197 回答
0

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

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

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

于 2008-09-24T13:27:15.153 回答