代码之家  ›  专栏  ›  技术社区  ›  rafek

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

  •  12
  • rafek  · 技术社区  · 15 年前

    考虑以下服务块:

    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: 给定的测试是坏的,因为产品和退回的产品实际上是相同的参考。

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

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

    提示4: 我不是在测试sut不做什么。我是在测试sut是否返回与从存储库返回的对象相同的对象。

    12 回复  |  直到 15 年前
        1
  •  9
  •   Stefan Steinegger    15 年前

    就我个人而言,我不在乎这个。测试应该确保代码按照您的意愿执行。 很难测试代码是什么 在这种情况下我不会麻烦的。

    测试实际上应该如下所示:

    [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);
    }
    

    我是说,这是你正在测试的一行代码。

        2
  •  3
  •   dss539    15 年前

    你为什么不嘲笑 product 以及 productRepository ?

    如果你嘲笑 产品 使用A 严格的 模拟,当存储库接触到您的产品时,您将失败。

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

        3
  •  3
  •   mdma    15 年前

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

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

    这是您的测试所验证的。它是未指定的,因为它没有提到是否允许修改。如果我们说

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

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

    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
    

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

    这体现了这样一个事实:所有者和其他产品属性必须等于生成器的原始值。这看起来像是我在说明显的,因为代码非常简单,但是如果您从需求规范的角度考虑,它运行得相当深入。

    我经常“测试我的测试”,规定“如果我更改这行代码,调整一两个临界常数,或者注入一些代码脉冲(例如更改!)=to==),哪个测试将捕获错误?”如果有一个测试捕捉到了问题,那么做是为了真正的发现。有时不是,在这种情况下,是时候看一下测试中隐含的需求,然后看看我们如何收紧它们。在没有实际需求捕获/分析的项目中,这是一个有用的工具,可以加强测试,使其在发生意外变化时失败。

    当然,你必须务实。你不能合理地期望处理所有的变更——有些只是荒谬的,程序会崩溃。但是,像所有者变更这样的逻辑变更是加强测试的好候选者。

    通过将需求的讨论拖拽到一个简单的代码修复中,有些人可能认为我走到了最深处,但是彻底的需求有助于产生彻底的测试,如果您没有需求,那么您需要加倍努力来确保您的测试是彻底的,因为您在编写测试时隐式地执行需求捕获。

    编辑:我是在回答问题的限制范围内回答这个问题的。如果可以自由选择,我建议不要使用EntityGenerator创建产品测试实例,而是“手工”创建它们,并使用相等比较。或者更直接地,将返回产品的字段与测试中的特定(硬编码)值进行比较,同样,在测试中不使用EntityGenerator。

        4
  •  2
  •   Gutzofter    15 年前

    呃……啊……啊……啊……

    问题1:不要更改代码,然后编写一个测试。首先为预期的行为编写测试。然后你可以做任何你想做的事情。

    问题2:您不会在 Product 更改产品所有者的网关。你改变了你的模型。

    但是如果你坚持,那就听你的测试。他们告诉您,您有可能从拥有不正确所有者的网关中提取产品。噢,看起来像是商业规则。应该在模型中进行测试。

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

    如果您以这种方式测试,您将创建脆弱的测试。如果产品进一步改变怎么办?现在到处都是不及格的测试。

    您的产品(模型)消费者是唯一关心 产品 .

    所以您的网关测试应该如下所示:

    [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();
    }
    

    不要把业务逻辑放在它不属于的地方!它的推论是不要在没有业务逻辑的地方测试业务逻辑。

        5
  •  1
  •   Jeff Sternal    15 年前

    如果您真的想保证服务方法不会改变您产品的属性,您有两个选择:

    • 在测试中定义预期的产品属性,并断言结果产品与这些值匹配。(通过克隆对象,这似乎就是您现在所做的。)

    • 嘲弄 产品 并指定期望值以验证服务方法不会更改其属性。

    下面是我对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();
    }
    
        6
  •  1
  •   Frank Schwieterman    15 年前

    我之前的回答是这样的,尽管它假定您关心的产品类的成员是公共的和虚拟的。如果类是POCO/DTO,则不太可能出现这种情况。

    您要查找的内容可能会被重新表述为一种比较对象值(而不是实例)的方法。

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

    我写了一些实用函数…assert2.issameValue(预期值,实际值),其功能类似于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;
            });
        }
    }
    
        7
  •  0
  •   Frank Schwieterman    15 年前

    好吧,一种方法是传递一个模拟的产品,而不是实际的产品。通过严格要求来验证没有任何影响产品的因素。(我想你是在使用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();
    }
    

    也就是说,我不确定你应该这么做。测试做了很多工作,可能表明在某个地方还有另一个需求。找到那个需求并创建第二个测试。也许你只是想阻止自己做傻事。我不认为那是天平,因为你能做那么多蠢事。尝试测试每一个都会花费太长时间。

        8
  •  0
  •   TcKs    15 年前

    我不确定单元测试是否应该关注“给定方法的作用” “。有无数的步骤是可能的。严格来说,测试“getproduct(id)返回与productrepository上getproduct(id)相同的产品”是正确的 product.Owner = "totallyDifferentOwner" .

    但是,您可以创建一个测试(如果需要)“getproduct(id)return product with same content as getproduct(id)on productrepository”(getproduct(id)return product),您可以在其中创建一个产品实例的(适当深度)克隆,然后应该比较两个对象的内容(因此没有object.equals或object.referenceequals)。

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

        9
  •  0
  •   Sean B    15 年前

    您可以将接口返回到产品,而不是具体的产品。

    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);
       }
    }
    

    享受。

        10
  •  0
  •   guillaume31    15 年前

    如果productService.getProduct()的所有使用者都希望得到与从productRepository请求相同的结果,为什么不直接调用productRepository.getProduct()本身呢? 你好像有一个不想要的 Middle Man 在这里。

    没有为productService.getProduct()添加太多的值。转储它,让客户机对象直接调用productrepository.getproduct()。将错误处理和日志记录到productrepository.getproduct()或使用者代码(可能通过AOP)。

    不再有中间人,不再有差异问题,不再需要测试这个差异。

        11
  •  0
  •   roufamatic RichardJohnn    15 年前

    我来说明一下我看到的问题。

    1. 你有一个方法和一个测试方法。测试方法验证了原始方法。
    2. 您可以通过更改数据来更改正在测试的系统。您想看到的是相同的单元测试失败了。

    所以实际上,您正在创建一个测试,该测试在服务层返回数据源后验证数据源中的数据是否与提取的对象中的数据匹配。这可能属于“集成测试”的范畴。

    在这种情况下,你没有很多好的选择。最后,您希望知道每个属性都与一些传入的属性值相同。所以你必须独立测试每个属性。您可以通过反射来实现这一点,但对于嵌套集合来说,这并不理想。

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

    您最好对数据层编写一个测试,以确保它正确地获取数据,然后使用单元测试来检查服务层中的业务逻辑。如果您对这种方法的更多细节感兴趣,请在评论中回复。

        12
  •  0
  •   SlavaGu    15 年前

    查看所有提供的4个提示后,您似乎希望在运行时使对象不可变。C语言不支持这一点。只有重构产品类本身才有可能。对于重构,您可以 IReadonlyProduct 接近并保护所有setter不被调用。但是,这仍然允许修改容器的元素,例如 List<> 由getters返回。只读集合也不起作用。只有wpf允许您在运行时使用 Freezable 班级。

    所以,我看到确保对象具有相同内容的唯一正确方法是比较它们。可能最简单的方法是 [Serializable] 属性为所有相关实体,并按照Frank Schwiterman的建议进行序列化和比较。