【发布时间】:2009-11-11 18:14:29
【问题描述】:
我使用过编写了 NUnit 测试的代码。但是,我从未使用过模拟框架。这些是什么?我了解依赖注入以及它如何帮助提高可测试性。我的意思是在单元测试时可以模拟所有依赖项。但是,那为什么我们需要模拟框架呢?我们不能简单地创建模拟对象并提供依赖项。我在这里错过了什么吗? 谢谢。
【问题讨论】:
我使用过编写了 NUnit 测试的代码。但是,我从未使用过模拟框架。这些是什么?我了解依赖注入以及它如何帮助提高可测试性。我的意思是在单元测试时可以模拟所有依赖项。但是,那为什么我们需要模拟框架呢?我们不能简单地创建模拟对象并提供依赖项。我在这里错过了什么吗? 谢谢。
【问题讨论】:
这里有一个例子:
var extension = MockRepository
.GenerateMock<IContextExtension<StandardContext>>();
var ctx = new StandardContext();
ctx.AddExtension(extension);
extension.AssertWasCalled(
e=>e.Attach(null),
o=>o.Constraints(Is.Equal(ctx)));
你可以看到我明确地测试了 IContextExtension 的 Attach 方法被调用并且输入参数是上下文对象。如果不这样的话,我的测试就会失败。
【讨论】:
您可以手动创建模拟对象并在使用依赖注入框架进行测试期间使用它们...但是让模拟框架为您生成模拟对象可以节省时间。
与往常一样,如果使用框架增加了太多复杂性而无用,请不要使用它。
【讨论】:
有时在使用第三方库时,甚至在使用 .NET 框架的某些方面时,在某些情况下编写测试非常困难 - 例如,HttpContext 或 Sharepoint 对象。为这些创建模拟对象可能会变得非常麻烦,因此模拟框架会处理基础知识,这样我们就可以将时间专注于使我们的应用程序与众不同的地方。
【讨论】:
使用模拟框架提供模拟比实际为每个要模拟的对象创建模拟对象更轻量级和简单得多。
例如,模拟框架对于验证是否进行了调用(或什至调用了多少次)等操作特别有用。制作你自己的模拟对象来检查这样的行为(而模拟行为本身就是一个主题)很乏味,而且是你引入错误的另一个地方。
查看 Rhino Mocks 以了解模拟框架的强大功能。
【讨论】:
模拟对象代替您的代码需要访问才能运行的任何大型/复杂/外部对象。
它们是有益的,原因如下:
您的测试旨在快速轻松地运行。如果您的代码依赖于数据库连接,那么您需要运行一个完全配置和填充的数据库才能运行您的测试。这可能会很烦人,因此您创建一个替换 - 一个“模拟” - 数据库连接对象,它只是模拟数据库。
您可以准确控制 Mock 对象的输出,因此可以将它们用作测试的可控数据源。
您可以在创建真实对象之前创建模拟对象,以优化其界面。这在测试驱动开发中很有用。
【讨论】:
使用模拟库的唯一原因是它使模拟更容易。
当然,你可以在没有库的情况下完成这一切,如果它很简单那很好,但是一旦它们开始变得复杂,库就容易多了。
从排序算法的角度考虑这个问题,当然任何人都可以编写,但为什么呢?如果代码已经存在并且易于调用......为什么不使用它呢?
【讨论】:
您当然可以手动模拟您的依赖项,但是使用框架可以省去很多繁琐的工作。此外,通常可用的断言也值得学习。
【讨论】:
模拟框架允许您将要测试的代码单元与该代码的依赖项隔离开来。它们还允许您在测试环境中模拟代码依赖项的各种行为,否则这些行为可能难以设置或重现。
例如,如果我有一个包含我希望测试的业务规则和逻辑的类 A,但是这个类 A 依赖于数据访问类、其他业务类,甚至 u/i 类等,这些其他类可以模拟以某种方式执行(或者在松散模拟行为的情况下根本不执行),以基于这些其他类在生产环境中可以想象的行为的所有可以想象的方式来测试 A 类中的逻辑。
举个更深层次的例子,假设你的 A 类调用了一个数据访问类的方法,比如
public bool IsOrderOnHold(int orderNumber) {}
然后可以将该数据访问类的模拟设置为每次返回 true 或每次返回 false,以测试您的 A 类如何响应这种情况。
【讨论】:
我声称你没有。 10 次中有 9 次编写测试替身并不是一件大事。大多数情况下,它几乎完全是自动完成的,只需要求 resharper 为您实现一个接口,然后您只需添加该替身所需的次要细节(因为您不是在做一堆逻辑并创建这些复杂的超级测试替身,对吗?对吧?)
“但是我为什么要让我的测试项目因一堆测试替身而臃肿”你可能会问。好吧,你不应该。 DRY 原则也适用于测试。创建可重复使用且具有描述性名称的 GOOD 测试替身。这也使您的测试更具可读性。
让事情变得更加困难的一件事是过度使用测试替身。我倾向于同意 Roy Osherove 和 Uncle Bob 的观点,你真的不想经常创建具有一些特殊配置的模拟对象。这本身就是一种设计气味。使用一个框架很容易在几乎每个测试中使用具有复杂逻辑的测试替身,最后你发现你还没有真正测试过你的生产代码,你只是测试了可怕的科学怪人的怪物般的嘲笑包含包含更多模拟的模拟。如果您自己编写双打,就永远不会“意外”这样做。
当然,有人会指出,有时你“不得不”使用框架,不这样做是很愚蠢的。当然,也有这样的情况。但你可能没有这种情况。大部分人不会,而且只是一小部分代码,或者代码本身真的很糟糕。
我建议任何人(尤其是初学者)远离框架并学习如何在没有它们的情况下生活,然后当他们觉得他们真的必须这样做时,他们可以使用他们认为最合适的任何框架,但到那时,这将是一个明智的决定,他们将不太可能滥用框架来创建糟糕的代码。
【讨论】:
模拟框架让我的生活更轻松,也更不乏味,因此我可以花时间实际编写代码。以 Mockito 为例(在 Java 世界中)
//mock creation
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
//stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
//stubbing using hamcrest (let's say isValid() returns your own hamcrest matcher):
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
//following prints "element"
System.out.println(mockedList.get(999));
虽然这是一个人为的例子,如果你用MyComplex.class 替换List.class,那么拥有一个模拟框架的价值就变得很明显了。你可以自己写,也可以不写,但你为什么要走那条路。
【讨论】:
当我比较为一组单元测试手动编写测试替身时,我首先想到了为什么我需要一个模拟框架(每个测试需要稍微不同的行为,所以我为每个测试创建了基本假类型的子类)使用 RhinoMocks 或 Moq 之类的东西来做同样的工作。
简单地说,使用框架来生成我需要的所有假对象比手动编写(和调试)我自己的假对象要快得多。
【讨论】: