在单元测试中模拟依赖项有什么好处?

时间:2014-04-01 10:54:50

标签: c# unit-testing mocking

我正在为我的控制器和服务层(C#,MVC)进行单元测试。我正在使用Moq DLL来模拟单元测试中的真实/依赖对象。

但是我对于模拟依赖项或真实对象有点困惑。让我们举一个下面单元测试方法的例子: -

[TestMethod]
public void ShouldReturnDtosWhenCustomersFound_GetCustomers ()
{
    // Arrrange 
    var name = "ricky";
    var description = "this is the test";

    // setup mocked dal to return list of customers
    // when name and description passed to GetCustomers method
    _customerDalMock.Setup(d => d.GetCustomers(name, description)).Returns(_customerList);

    // Act
    List<CustomerDto> actual = _CustomerService.GetCustomers(name, description);

    // Assert
    Assert.IsNotNull(actual);
    Assert.IsTrue(actual.Any());

    // verify all setups of mocked dal were called by service
    _customerDalMock.VerifyAll();
}

在上面的单元测试方法中,我正在模拟GetCustomers方法并返回一个客户列表。哪个已定义。看起来如下:

List<Customer> _customerList = new List<Customer>
{
    new Customer { CustomerID = 1, Name="Mariya",Description="description"},
    new Customer { CustomerID = 2, Name="Soniya",Description="des"},
    new Customer { CustomerID = 3, Name="Bill",Description="my desc"},
    new Customer { CustomerID = 4, Name="jay",Description="test"},
};

让我们来看看客户的Assertion模拟对象和实际对象断言: -

Assert.AreEqual(_customer.CustomerID, actual.CustomerID);
Assert.AreEqual(_customer.Name, actual.Name);
Assert.AreEqual(_customer.Description, actual.Description);

但在这里我并不理解它(在单元测试之上)总能正常工作。意味着我们只是测试(在Assertion中)我们传递的或我们正在返回的(在模拟对象中)。我们知道真实/实际对象将始终返回我们传递的列表或对象。

那么在这里进行单元测试或嘲笑是什么意思?

4 个答案:

答案 0 :(得分:6)

嘲笑的真正目的是实现真正的孤立。

假设您有一个CustomerService课程,这取决于CustomerRepository。您编写了一些单元测试,涵盖CustomerService提供的功能。他们都过去了。

一个月后,进行了一些更改,突然您的CustomerServices单元测试开始失败 - 您需要找到问题所在。

所以你假设:

  

由于测试CustomerServices的单元测试失败,问题必定在该类中!

右?错误!问题可能出在CustomerServices或其任何依赖项中,即CustomerRepository。如果它的任何依赖项失败,那么被测试的类也可能会失败。

现在描绘了一大堆依赖关系:A取决于BB取决于C,... Y取决于{{1} }}。如果在Z所有中引入了错误,则您的单元测试将失败。

这就是为什么你需要将测试中的类与其依赖项隔离开来(可能是域对象,数据库连接,文件资源等)。您想测试单元

答案 1 :(得分:1)

你的例子过于简单化,无法展示嘲笑的真正好处。那是因为你所测试的逻辑除了返回一些数据之外并没有做太多的事情。

但是想象一下,你的逻辑基于挂钟时间做了一些事情,比如每小时安排一些过程。在这样的情况下,模拟时间源可以让你实际单元测试这样的逻辑,这样你的测试就不必运行几个小时,等待时间过去。

答案 2 :(得分:1)

除了已经说过的话:

我们可以拥有没有依赖关系的类。我们唯一拥有的是没有模拟和存根的单元测试。

当我们有依赖关系时,它们有几种:

  • 我们班级主要以“一劳永逸”的方式使用的服务,即不影响消费代码控制流程的服务。

我们可以模拟这些(以及所有其他类型)服务来测试它们是否被正确调用(集成测试)或仅仅用于注入,因为我们的代码可能需要它们。

  • 提供结果但没有内部的双向服务 状态并且不影响系统的状态。它们可以被称为复杂的数据转换。

通过模拟这些服务,您可以测试您对服务实现的不同变体的代码行为的期望,而无需将所有服务实现。(

  • 影响系统状态或依赖现实世界的服务 现象或某些不受你控制的事情。 '@ 500 - 内部服务器错误'给出了时间服务的一个很好的例子。

通过模拟,您可以让时间以所需的速度(和方向)流动。
另一个例子是使用DB。在进行单元测试时,通常不希望改变DB状态,而功能测试则不然。对于这种服务,“隔离”是嘲弄的主要动机(但不是唯一的动机)。

  • 您的代码所依赖的内部状态服务。

考虑实体框架:
调用SaveChanges()时,场景后面会发生很多事情。 EF检测更改和修正导航属性。此外,EF不允许您添加具有相同密钥的多个实体 显然,模仿这种依赖的行为和复杂性是非常困难的...但通常你没有设计得好。如果您严重依赖某些组件提供的功能,则几乎无法替代此依赖项。可能需要的是再次隔离。您不希望在测试时留下痕迹,因此黄油方法是告诉EF不要使用真正的DB。是的,依赖意味着不仅仅是一个界面。更常见的不是方法签名,而是预期行为的合同。例如,IDbConnection具有Open()Close()方法,这意味着某些调用序列。

当然,这不是严格的分类。最好将其视为极端。

@dcastro写道:You want to test a unit.然而,声明并没有回答你是否应该回答的问题 让我们不要打折整合测试。有时候知道系统的某些复合部分出现故障是可以的 至于@dcastro给出的依赖链,我们可以尝试找到包可能所在的地方:

假设Z是最终的依赖。我们创建没有模拟的单元测试。所有边界条件都是已知的。这里必须100%覆盖。之后我们说Z正常工作。如果Z失败,我们的单元测试必须表明它 模拟来自工程。在建造飞机时,没有人测试每个螺钉和螺栓。
统计方法用于证明工厂生产细节的工作正常。

另一方面,对于系统中非常关键的部分,花时间和模拟依赖的复杂行为是合理的。是的,可维护性较差的测试越复杂。在这里,我宁愿把它们称为规范检查 是的,您的api和测试都可能是错误的,但代码审查和其他形式的测试可以在某种程度上确保代码的正确性。一旦这些测试在进行一些更改后失败,您需要更改规范和相应的测试,或者找到错误并通过测试覆盖案例。

我强烈建议您观看Roy的视频:http://youtube.com/watch?v=fAb_OnooCsQ

答案 3 :(得分:0)

在这种情况下,模拟允许您伪造数据库连接,以便您可以在适当的位置和内存中运行测试,而无需依赖任何其他资源,即数据库。此测试断言,在调用服务时,会调用相应的DAL方法。

然而,列表的后续断言和列表中的值是不必要的。正如你正确地注意到你只是断言你所嘲笑的价值&#34;被退回。这在模拟框架本身中很有用,可以断言模拟方法的行为符合预期。但是在你的代码中只是过剩。

一般情况下,模拟允许一个人:

  • 测试行为(当事情发生时,然后执行特定方法)
  • 虚假资源(例如,电子邮件服务器,Web服务器,HTTP API请求/响应,数据库)

相比之下,没有模拟的单元测试通常允许您测试状态。也就是说,当调用特定方法时,您可以检测对象状态的变化。