单元测试,模拟 - 简单案例:服务 - 存储库

时间:2010-05-08 06:29:23

标签: c# unit-testing service mocking repository

考虑以下服务块:

public class ProductService : IProductService {

   private IProductRepository _productRepository;

   // Some initlization stuff

   public Product GetProduct(int id) {
      try {
         return _productRepository.GetProduct(id);
      } catch (Exception e) {
         // log, wrap then throw
      }
   }
}

让我们考虑一个简单的单元测试:

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = EntityGenerator.Product();

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreEqual(product, returnedProduct);

   _productRepositoryMock.VerifyAll();
}

起初看起来这个测试还可以。但是让我们改变一下我们的服务方法:

public Product GetProduct(int id) {
   try {
      var product = _productRepository.GetProduct(id);

      product.Owner = "totallyDifferentOwner";

      return product;
   } catch (Exception e) {
      // log, wrap then throw
   }
}

如何重写使用第一种服务方法传递的给定测试并使用第二种服务方法失败?

您如何处理这种简单方案?

提示1:给定的测试是错误的coz产品,而returnProduct实际上是相同的参考。

提示2:实现平等成员(object.equals)不是解决方案。

提示3:至于现在,我使用AutoMapper创建了Product实例(expectedProduct)的克隆 - 但我不喜欢这个解决方案。

提示4:我没有测试SUT不做某事。我正在尝试测试SUT DOES返回与从存储库返回的对象相同的对象。

12 个答案:

答案 0 :(得分:9)

就个人而言,我不会在乎这个。测试应该确保代码正在执行您的意图。 很难测试哪些代码正在做,在这种情况下我不会打扰。

测试实际上应该是这样的:

[Test]
public void GetProduct_GetsProductFromRepository() 
{
   var product = EntityGenerator.Product();

   _productRepositoryMock
     .Setup(pr => pr.GetProduct(product.Id))
     .Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreSame(product, returnedProduct);
}

我的意思是,这是您正在测试的一行代码。

答案 1 :(得分:3)

单元测试的一种思考方式是编码规范。当您使用EntityGenerator为测试和实际服务生成实例时,可以看到您的测试表达了需求

  • 服务使用EntityGenerator生成产品实例。

这是您的测试验证的内容。它没有具体说明,因为它没有提到是否允许修改。如果我们说

  • 服务使用EntityGenerator生成无法修改的Product实例。

然后我们得到一个关于捕获错误所需的测试更改的提示:

var product = EntityGenerator.Product();
// [ Change ] 
var originalOwner = product.Owner;  
// assuming owner is an immutable value object, like String
// [...] - record other properties as well.

Product returnedProduct = _productService.GetProduct(product.Id);

Assert.AreEqual(product, returnedProduct);

// [ Change ] verify the product is equivalent to the original spec
Assert.AreEqual(originalOwner, returnedProduct.Owner);
// [...] - test other properties as well

(更改是我们从新创建的产品中检索所有者,并从服务返回的产品中检查所有者。)

这体现了所有者和其他产品属性必须等于生成器的原始值。这可能看起来像我说的那么明显,因为代码非常简单,但如果你考虑需求规范,它会运行得很深。

我经常“测试我的测试”,规定“如果我改变这行代码,调整一两个关键常数,或注入几个代码打点(例如更改!= = =),哪个测试将捕获错误?”如果有测试可以捕获问题,那么就可以找到它。有时候不是,在这种情况下,是时候查看测试中隐含的要求了,看看我们如何收紧它们。在没有实际需求捕获/分析的项目中,这可以是一个有用的工具来强化测试,以便在发生意外更改时失败。

当然,你必须务实。您无法合理地期望处理所有更改 - 有些只是荒谬而且程序会崩溃。但是,所有者改变等逻辑变化是加强测试的良好候选者。

通过将需求的讨论拖到一个简单的编码修复中,有些人可能会认为我已经走到了尽头,但是全面的要求有助于产生全面的测试,如果你没有要求,那么你需要加倍努力工作确保您的测试是彻底的,因为您在编写测试时隐式执行需求捕获。

编辑:我正在问题中设定的约束内回答这个问题。给定一个自由选择,我建议不要使用EntityGenerator来创建Product测试实例,而是“手动”创建它们并使用相等比较。或者更直接,将测试中返回的Product的字段与测试中的特定(硬编码)值进行比较,而不在测试中使用EntityGenerator。

答案 2 :(得分:3)

为什么不模仿product以及productRepository

如果您使用严格模拟模拟product,则当存储库触及您的产品时,您将会失败。

如果这是一个完全荒谬的想法,你能解释一下原因吗?老实说,我想学习。

答案 3 :(得分:2)

Uhhhhhhhhhhh ...................

Q1:不要对代码进行更改然后编写测试。首先为预期的行为编写测试。那么你可以为SUT做任何你想做的事。

Q2:您未在Product网关中进行更改以更改产品的所有者。您可以在模型中进行更改。

但如果你坚持,那就听听你的测试吧。他们告诉您,您可以从具有错误所有者的网关中提取产品。哎呀,看起来像一个商业规则。应该在模型中进行测试。

另外你使用模拟。为什么要测试实现细节?网关只关心_productRepository.GetProduct(id)返回产品。不是产品是什么。

如果以这种方式进行测试,您将创建脆弱的测试。如果产品进一步变化怎么办现在你到处都有失败的测试。

您的产品消费者(MODEL)是唯一关心Product实施的人。

所以你的网关测试应该是这样的:

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = EntityGenerator.Product();

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   _productService.GetProduct(product.Id);

   _productRepositoryMock.VerifyAll();
}

不要将业务逻辑放在不属于的地方!它的必然结果是不测试应该没有的业务逻辑。

答案 4 :(得分:1)

如果您确实希望保证服务方法不会更改产品的属性,您有两种选择:

  • 在测试中定义预期的产品属性,并声明生成的产品与这些值匹配。 (这似乎是你现在通过克隆对象所做的事情。)

  • 模拟产品并指定期望以验证服务方法是否不会更改其属性。

这就是我用NMock做后者的方法:

// If you're not a purist, go ahead and verify all the attributes in a single
// test - Get_Product_Does_Not_Modify_The_Product_Returned_By_The_Repository
[Test]
public Get_Product_Does_Not_Modify_Owner() {

    Product mockProduct = mockery.NewMock<Product>(MockStyle.Transparent);

    Stub.On(_productRepositoryMock)
        .Method("GetProduct")
        .Will(Return.Value(mockProduct);

    Expect.Never
          .On(mockProduct)
          .SetProperty("Owner");

    _productService.GetProduct(0);

    mockery.VerifyAllExpectationsHaveBeenMet();
}

答案 5 :(得分:1)

我之前的回答是,虽然它假设您关心的Product类的成员是公共的和虚拟的。如果该类是POCO / DTO,则不太可能。

您正在寻找的内容可能会被重新定义为比较对象的值(而不是实例)的方法。

一种比较方式,以查看序列化时是否匹配。我最近为一些代码做了这个...用参数化对象替换长参数列表。代码是苛刻的,我不想重构它,尽管它很快就会消失。所以我只是将这种序列化比较作为一种快速方法来查看它们是否具有相同的值。

我编写了一些实用函数... Assert2.IsSameValue(expected,actual),它的功能类似于NUnit的Assert.AreEqual(),除了它在比较之前通过JSON序列化。同样,It2.IsSameSerialized()可用于以类似于Moq.It.Is()的方式描述传递给模拟调用的参数。

public class Assert2
{
    public static void IsSameValue(object expectedValue, object actualValue) {

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        var expectedJSON = serializer.Serialize(expectedValue);
        var actualJSON = serializer.Serialize(actualValue);

        Assert.AreEqual(expectedJSON, actualJSON);
    }
}

public static class It2
{
    public static T IsSameSerialized<T>(T expectedRecord) {

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        string expectedJSON = serializer.Serialize(expectedRecord);

        return Match<T>.Create(delegate(T actual) {

            string actualJSON = serializer.Serialize(actual);

            return expectedJSON == actualJSON;
        });
    }
}

答案 6 :(得分:0)

嗯,一种方法是传递模拟产品而不是实际产品。通过严格限制验证不会影响产品。 (我假设您使用的是Moq,看起来像是你)

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = new Mock<EntityGenerator.Product>(MockBehavior.Strict);

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreEqual(product, returnedProduct);

   _productRepositoryMock.VerifyAll();
   product.VerifyAll();
}

那就是说,我不确定你应该这样做。测试做得很多,可能表明某处有另一个要求。找到该要求并创建第二个测试。可能你只是想阻止自己做一些愚蠢的事情。我不认为这个尺度,因为你可以做很多蠢事。试图测试每个都需要太长时间。

答案 7 :(得分:0)

我不确定,如果单元测试应该关心“给定的方法”。有许多可能的步骤。严格来说,测试“GetProduct(id)返回与productRepository上的getProduct(id)相同的产品”是正确的,有或没有行product.Owner = "totallyDifferentOwner"

但是,您可以创建一个测试(如果需要)“GetProduct(id)返回与productRepository上的getProduct(id)相同内容的产品”,您可以在其中创建一个(可以深入)克隆一个产品实例,然后您应该比较两个对象的内容(所以没有object.Equals或object.ReferenceEquals)。

单元测试不保证100%无错误和正确的行为。

答案 8 :(得分:0)

您可以将界面返回到产品而不是具体的产品。

public IProduct GetProduct(int id) 
{ 
   return _productRepository.GetProduct(id);
}

然后验证未设置所有者属性:

Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg.Is.Anything);

如果您关心所有属性和/或方法,那么Rhino可能存在预先存在的方式。否则,您可以创建一个可能使用反射的扩展方法,例如:

Dep<IProduct>().AssertNoPropertyOrMethodWasCalled()

我们的行为规范如下:

[Specification]
public class When_product_service_has_get_product_called_with_any_id 
       : ProductServiceSpecification
{
   private int _productId;

   private IProduct _actualProduct;

   [It] 
   public void Should_return_the_expected_product()
   {
     this._actualProduct.Should().Be.EqualTo(Dep<IProduct>());
   }

   [It]
   public void Should_not_have_the_product_modified()
   {
     Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg<string>.Is.Anything);

     // or write your own extension method:
     // Dep<IProduct>().AssertNoPropertyOrMethodWasCalled();
   }


   public override void GivenThat()
   {
     var randomGenerator = new RandomGenerator();
     this._productId = randomGenerator.Generate<int>();

     Stub<IProductRepository, IProduct>(r => r.GetProduct(this._productId));
   }

   public override void WhenIRun()
   {
       this._actualProduct = Sut.GetProduct(this._productId);
   }
}

享受。

答案 9 :(得分:0)

如果ProductService.GetProduct()的所有使用者都期望得到与他们从ProductRepository中询问的结果相同的结果,为什么他们不只是调用ProductRepository.GetProduct()本身? 看来你这里有一个不受欢迎的Middle Man

ProductService.GetProduct()没有太多增值。转储它并让客户端对象直接调用ProductRepository.GetProduct()。将错误处理和日志记录放入ProductRepository.GetProduct()或使用者代码(可能通过AOP)。

不再有中间人,没有更多的差异问题,不再需要测试这种差异。

答案 10 :(得分:0)

让我按照我的意见陈述问题。

  1. 您有方法和测试方法。测试方法验证了原始方法。
  2. 您可以通过更改数据来更改被测系统。您希望看到的是相同的单元测试失败。
  3. 因此,实际上您正在创建一个测试,用于验证数据源中的数据是否与服务层返回后的数据源中的数据相匹配。这可能属于“整合测试”的范畴。

    在这种情况下,你没有很多好的选择。最终,您想要知道每个属性都与某些传入的属性值相同。所以你被迫独立测试每个属性。您可以使用反射执行此操作,但这对嵌套集合不起作用。

    我认为真正的问题是:为什么要测试服务模型以确保数据层的正确性,以及为什么在服务模型中编写代码只是为了打破测试?您是否担心您或其他用户可能会将对象设置为服务层中的无效状态?在这种情况下,您应该更改合同,以便Product.Owner为readonly

    最好针对数据层编写测试以确保它正确地获取数据,然后使用单元测试来检查服务层中的业务逻辑。如果您对评论中有关此方法回复的更多详细信息感兴趣。

答案 11 :(得分:0)

查看所有4个提示,似乎您希望在运行时使对象不可变。 C#语言不支持。只有重构Product类本身才有可能。对于重构,您可以采用IReadonlyProduct方法并保护所有setter不被调用。但是,这仍然允许修改由getter返回的容器元素,如List<>。 ReadOnly集合也无济于事。只有WPF允许您在运行时使用Freezable类更改不变性。

所以我看到通过比较它们来确保对象具有相同内容的唯一正确方法。可能最简单的方法是将[Serializable]属性添加到所有涉及的实体,并按照Frank Schwieterman的建议进行序列化与比较。