为了帮助我的团队编写可测试的代码,我提出了这个简单的最佳实践列表,以使我们的C#代码库更易于测试。 (其中一些要点指的是Rhino Mocks的限制,这是C#的模拟框架,但规则也可能更普遍适用。)有没有人有他们遵循的最佳实践?
要最大限度地提高代码的可测试性,请遵循以下规则:
首先编写测试,然后编写代码。原因:这可以确保您编写可测试的代码,并且每行代码都会为其编写测试。
使用依赖注入设计类。原因:您无法模拟或测试无法看到的内容。
使用Model-View-Controller或Model-View-Presenter将UI代码与其行为分开。原因:允许在无法测试的部件中测试业务逻辑(UI)被最小化。
不要编写静态方法或类。原因:静态方法难以或无法隔离,Rhino Mocks无法模拟它们。
编程关闭接口,而不是类。原因:使用接口可以明确对象之间的关系。接口应该定义对象从其环境中需要的服务。此外,可以使用Rhino Mocks和其他模拟框架轻松模拟接口。
隔离外部依赖项。原因:无法测试未解析的外部依赖项。
将您打算模拟的方法标记为虚拟。原因:Rhino Mocks无法模拟非虚拟方法。
答案 0 :(得分:58)
绝对是个好名单。以下是一些想法:
首先编写测试,然后编写代码。
我同意,在高层次上。但是,我会更具体:“首先编写测试,然后编写足够的代码以通过测试,然后重复。”否则,我担心我的单元测试看起来更像集成或验收测试。
使用依赖注入设计类。
同意。当对象创建自己的依赖项时,您无法控制它们。控制/依赖注入的反转为您提供了控制,允许您使用模拟/存根/等隔离测试对象。这是您单独测试对象的方法。
使用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's Arrange/Act/Assert technique编写测试。这项技术非常清楚需要哪些配置,实际测试内容以及预期内容。
不要害怕推出自己的模拟/存根。通常,您会发现使用模拟对象框架会使您的测试难以阅读。通过自己滚动,您可以完全控制您的模拟/存根,并且您将能够保持您的测试可读性。 (请参阅上一点。)
避免将单元测试中的重复重构为抽象基类或设置/拆卸方法的诱惑。这样做会隐藏开发人员尝试进行单元测试的配置/清理代码。在这种情况下,每个单独测试的清晰度比重构重复更重要。
实施持续集成。在每个“绿色栏”上签入您的代码。构建您的软件并在每次办理登机手续时运行全套的单元测试。 (当然,这本身并不是编码实践;但它是保持软件清洁和完全集成的绝佳工具。)
答案 1 :(得分:10)
如果您正在使用.Net 3.5,您可能需要查看Moq模拟库 - 它使用表达式树和lambdas来删除大多数其他模拟库的非直观记录回复习惯用法。
查看此quickstart以查看您的测试用例变得多么直观,这是一个简单的示例:
// 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));
答案 2 :(得分:6)
了解fakes, mocks and stubs与何时使用它们之间的区别。
避免使用模拟过度指定交互。这使得测试brittle。
答案 3 :(得分:3)
这是一篇非常有用的帖子!
我想补充一点,理解上下文和被测系统(SUT)总是很重要的。当您在现有代码遵循相同主体的环境中编写新代码时,遵循TDD原则会更容易。但是当您在非TDD遗留环境中编写新代码时,您会发现您的TDD工作可能会迅速超出您的预期和期望。
对于一些生活在完全学术世界中的人来说,时间表和交付可能并不重要,但在软件是金钱的环境中,有效利用TDD工作至关重要。
TDD受Diminishing Marginal Return法的高度约束。简而言之,在达到最大回报之前,您对TDD的努力越来越有价值,之后,投入TDD的后续时间价值越来越低。
我倾向于认为TDD的主要价值在边界(黑盒)以及系统关键任务区域的偶尔白盒测试中。
答案 4 :(得分:2)
针对接口编程的真正原因不是让Rhino的生活更轻松,而是澄清代码中对象之间的关系。接口应该定义对象从其环境中需要的服务。类提供该服务的特定实现。阅读Rebecca Wirfs-Brock关于角色,责任和合作者的“对象设计”一书。
答案 5 :(得分:1)
好的清单。你可能想要建立的一件事 - 我不能给你很多建议,因为我自己刚刚开始考虑它 - 是一个类应该在不同的库,命名空间,嵌套命名空间中。您甚至可能希望事先确定一个库和命名空间列表,并要求团队必须满足并决定合并两个/添加一个新的。
哦,只是想到我做的事情,你可能也想要。我通常有一个单元测试库,每个类策略都有一个测试夹具,每个测试进入相应的命名空间。我还倾向于有另一个测试库(集成测试?),它更多BDD style。这允许我编写测试来指出方法应该做什么,以及应用程序应该做什么。
答案 6 :(得分:0)
这是我想到的另一个我想做的事。
如果您打算从单元测试Gui而不是TestDriven.Net或NAnt运行测试,那么我发现将单元测试项目类型设置为控制台应用程序而不是库更容易。这允许您手动运行测试并在调试模式中逐步执行它们(上述TestDriven.Net实际上可以为您执行此操作)。
另外,我总是希望开放一个Playground项目来测试一些我不熟悉的代码和想法。这不应该检查到源代码管理中。更好的是,它应该只在开发人员的机器上的一个单独的源代码控制库中。