简体   繁体   English

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

[英]Unit testing, mocking - simple case: Service - Repository

Consider a following chunk of service: 考虑以下服务块:

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

Let's consider a simple unit test: 让我们考虑一个简单的单元测试:

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

At first it seems that this test is ok. 起初看起来这个测试还可以。 But let's change our service method a little bit: 但是让我们改变一下我们的服务方法:

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

      product.Owner = "totallyDifferentOwner";

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

How to rewrite a given test that it'd pass with the first service method and fail with a second one? 如何重写一个给定的测试,它通过第一个服务方法传递,而第二个服务方法失败?

How do you handle this kind of simple scenarios? 你如何处理这种简单的场景?

HINT 1: A given test is bad coz product and returnedProduct is actually the same reference. 提示1:给定的测试是错误的coz产品,而returnProduct实际上是相同的参考。

HINT 2: Implementing equality members (object.equals) is not the solution. 提示2:实现平等成员(object.equals)不是解决方案。

HINT 3: As for now, I create a clone of the Product instance (expectedProduct) with AutoMapper - but I don't like this solution. 提示3:至于现在,我使用AutoMapper创建了Product实例(expectedProduct)的克隆 - 但我不喜欢这个解决方案。

HINT 4: I'm not testing that the SUT does NOT do sth. 提示4:我没有测试SUT不做某事。 I'm trying to test that SUT DOES return the same object as it is returned from repository. 我正在尝试测试SUT DOES返回从存储库返回的相同对象。

Personally, I wouldn't care about this. 就个人而言,我不会在意这一点。 The test should make sure that the code is doing what you intend. 测试应该确保代码正在执行您的意图。 It's very hard to test what code is not doing , I wouldn't bother in this case. 很难测试代码没有做什么 ,在这种情况下我不会打扰。

The test actually should just look like this: 测试实际上应该是这样的:

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

I mean, it's one line of code you are testing. 我的意思是,这是你正在测试的一行代码。

One way of thinking of unit tests is as coded specifications. 单元测试的一种思考方式是编码规范。 When you use the EntityGenerator to produce instances both for the Test and for the actual service, your test can be seen to express the requirement 当您使用EntityGenerator为Test和实际服务生成实例时,可以看到您的测试表达了要求

  • The Service uses the EntityGenerator to produce Product instances. 该服务使用EntityGenerator生成产品实例。

This is what your test verifies. 这是您的测试验证的内容。 It's underspecified because it doesn't mention if modifications are allowed or not. 它没有具体说明,因为它没有提到是否允许修改。 If we say 如果我们说

  • The Service uses the EntityGenerator to produce Product instances, which cannot be modified. 该服务使用EntityGenerator生成无法修改的Product实例。

Then we get a hint as to the test changes needed to capture the error: 然后我们得到一个关于捕获错误所需的测试更改的提示:

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

(The change is that we retrieve the owner from the freshly created Product and check the owner from the Product returned from the service.) (更改是我们从新创建的产品中检索所有者,并从服务返回的产品中检查所有者。)

This embodies the fact that the Owner and other product properties must equal the the original value from the generator. 这体现了所有者和其他产品属性必须等于发电机的原始值。 This may seem like I'm stating the obvious, since the code is pretty trivial, but it runs quite deep if you think in terms of requirement specifications. 这可能看起来像我说的那么明显,因为代码非常简单,但如果你考虑需求规范,它会运行得很深。

I often "test my tests" by stipulating "if I change this line of code, tweak a critical constant or two, or inject a few code burps (eg changing != to ==), which test will capture the error?" 我经常“测试我的测试”,规定“如果我改变这行代码,调整一个或两个临界常数,或注入一些代码打点(例如改变!= ==),哪个测试会捕获错误?” Doing it for real finds if there is a test that captures the problem. 如果有测试可以捕获问题,那么就可以找到它。 Sometimes not, in which case it's time to look at the requirements implicit in the tests, and see how we can tighten them up. 有时候不是,在这种情况下,是时候查看测试中隐含的要求了,看看我们如何收紧它们。 In projects with no real requirements capture/analysis this can be a useful tool to toughen up tests so they fail when unexpected changes occur. 在没有实际需求捕获/分析的项目中,这可以是一个有用的工具来强化测试,以便在发生意外更改时失败。

Of course, you have to be pragmatic. 当然,你必须务实。 You can't reasonably expect to handle all changes - some will simply be absurd and the program will crash. 您无法合理地期望处理所有更改 - 有些只是荒谬而且程序会崩溃。 But logical changes like the Owner change are good candidates for test strengthening. 但是像所有者改变这样的逻辑变化是加强测试的良好候选者。

By dragging talk of requirements into a simple coding fix, some may think I've gone off the deep end, but thorough requirements help produce thorough tests, and if you have no requirements, then you need to work doubly hard to make sure your tests are thorough, since you're implicitly doing requirements capture as you write the tests. 通过将需求的讨论拖到一个简单的编码修复中,有些人可能认为我已经走出了深层次,但是全面的需求有助于产生全面的测试,如果你没有要求,那么你需要加倍努力以确保你的测试是彻底的,因为你在编写测试时隐式地执行需求捕获。

EDIT: I'm answering this from within the contraints set in the question. 编辑:我是在问题中设定的约束内回答这个问题。 Given a free choice, I would suggest not using the EntityGenerator to create Product test instances, and instead create them "by hand" and use an equality comparison. 给定一个自由选择,我建议不要使用EntityGenerator来创建Product测试实例,而是“手动”创建它们并使用相等比较。 Or more direct, compare the fields of the returned Product to specific (hard-coded) values in the test, again, without using the EntityGenerator in the test. 或者更直接,将测试中返回的Product的字段与测试中的特定(硬编码)值进行比较,而不使用测试中的EntityGenerator。

Why don't you mock the product as well as the productRepository ? 为什么不模拟product以及productRepository

If you mock the product using a strict mock, you will get a failure when the repository touches your product. 如果您使用严格模拟来模拟product ,那么当存储库触及您的产品时,您将会失败。

If this is a completely ridiculous idea, can you please explain why? 如果这是一个完全荒谬的想法,你能解释一下原因吗? Honestly, I'd like to learn. 老实说,我想学习。

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

Q1: Don't make changes to code then write a test. Q1:不要对代码进行更改然后编写测试。 Write a test first for the expected behavior. 首先为预期的行为编写测试。 Then you can do whatever you want to the SUT. 那么你可以做任何你想要的SUT。

Q2: You don't make the changes in your Product Gateway to change the owner of the product. Q2:您不在Product网关中进行更改以更改Product的所有者。 You make the change in your model. 您在模型中进行更改。

But if you insist, then listen to your tests. 但如果你坚持,那就听听你的测试吧。 They are telling you that you have the possibility for products to be pulled from the gateway that have the incorrect owners. 他们告诉您,您可以从具有错误所有者的网关中提取产品。 Oops, Looks like a business rule. 哎呀,看起来像一个商业规则。 Should be tested for in the model. 应该在模型中进行测试。

Also your using a mock. 还有你使用模拟。 Why are you testing an implementation detail? 为什么要测试实现细节? The gateway only cares that the _productRepository.GetProduct(id) returns a product. 网关只关心_productRepository.GetProduct(id)返回产品。 Not what the product is. 不是产品是什么。

If you test in this manner you will be creating fragile tests. 如果以这种方式进行测试,您将创建脆弱的测试。 What if product changes further. 如果产品进一步变化怎么办 Now you have failing tests all over the place. 现在你到处都有失败的测试。

Your consumers of product (MODEL) are the only ones that care about the implementation of Product . 您的产品消费者(MODEL)是唯一关心Product实施的人。

So your gateway test should look like this: 所以你的网关测试应该是这样的:

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

Don't put business logic where it doesn't belong! 不要把业务逻辑放在它不属于的地方! And it's corollary is don't test for business logic where there should be none. 它的必然结果是不测试应该没有的业务逻辑。

If you really want to guarantee that the service method doesn't change the attributes of your products, you have two options: 如果您确实希望保证服务方法不会更改产品的属性,您有两种选择:

  • Define the expected product attributes in your test and assert that the resulting product matches these values. 在测试中定义预期的产品属性,并声明生成的产品与这些值匹配。 (This appears to be what you're doing now by cloning the object.) (这似乎是你现在通过克隆对象所做的事情。)

  • Mock the product and specify expectations to verify that the service method does not change its attributes. 模拟产品并指定期望以验证服务方法不会更改其属性。

This is how I'd do the latter with NMock: 这就是我用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();
}

My previous answer stands, though it assumes the members of the Product class that you care about are public and virtual. 我之前的回答是,尽管它假设您关心的Product类的成员是公共的和虚拟的。 This is not likely if the class is a POCO / DTO. 如果该类是POCO / DTO,则不太可能。

What you're looking for might be rephrased as a way to do comparison of the values (not instance) of the object. 您正在寻找的内容可能会被重新定义为比较对象的值(非实例)的方法。

One way to compare to see if they match when serialized. 比较的一种方法是查看序列化时是否匹配。 I did this recently for some code... Was replacing a long parameter list with a parameterized object. 我最近为一些代码做了这个...用参数化对象替换长参数列表。 The code is crufty, I don't want to refactor it though as its going away soon anyhow. 代码是苛刻的,我不想重构它,尽管它很快就会消失。 So I just do this serialization comparison as a quick way to see if they have the same value. 所以我只是将这种序列化比较作为一种快速方法来查看它们是否具有相同的值。

I wrote some utility functions... Assert2.IsSameValue(expected,actual) which functions like NUnit's Assert.AreEqual(), except it serializes via JSON before comparing. 我写了一些实用函数... Assert2.IsSameValue(expected,actual),它的功能类似于NUnit的Assert.AreEqual(),除了它在比较之前通过JSON序列化。 Likewise, It2.IsSameSerialized() can be used to describe parameters passed to mocked calls in a manner similar to Moq.It.Is(). 同样,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;
        });
    }
}

Well, one way is to pass around a mock of product rather than the actual product. 嗯,一种方法是传递模拟产品而不是实际产品。 Verify nothing to affect the product by making it strict. 通过严格限制验证不会影响产品。 (I assume you are using Moq, it looks like you are) (我假设你使用的是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();
}

That said, I'm not sure you should be doing this. 那就是说,我不确定你应该这样做。 The test is doing to much, and might indicate there is another requirement somewhere. 测试做得很多,可能表明某处有另一个要求。 Find that requirement and create a second test. 找到该要求并创建第二个测试。 It might be that you just want to stop yourself from doing something stupid. 可能你只是想阻止自己做一些愚蠢的事情。 I don't think that scales, because there are so many stupid things you can do. 我不认为这个尺度,因为你可以做很多蠢事。 Trying to test each would take too long. 试图测试每个将花费太长时间。

I'm not sure, if the unit test should care about "what given method does not ". 我不确定,如果单元测试应该关心“给定的方法没有 ”。 There are zillion steps which are possible. 有许多可能的步骤。 In strict the test "GetProduct(id) return the same product as getProduct(id) on productRepository" is correct with or without the line product.Owner = "totallyDifferentOwner" . 严格来说,测试“GetProduct(id)返回与productRepository上的getProduct(id)相同的产品”是否正确,有或没有行product.Owner = "totallyDifferentOwner"

However you can create a test (if is required) "GetProduct(id) return product with same content as getProduct(id) on productRepository" where you can create a (propably deep) clone of one product instance and then you should compare contents of the two objects (so no object.Equals or object.ReferenceEquals). 但是,您可以创建一个测试(如果需要)“GetProduct(id)返回与productRepository上的getProduct(id)具有相同内容的产品”,您可以在其中创建一个(可以深入)克隆一个产品实例,然后您应该比较内容这两个对象(所以没有object.Equals或object.ReferenceEquals)。

The unit tests are not guarantee for 100% bug free and correct behaviour. 单元测试不保证100%无错误和正确的行为。

You can return an interface to product instead of a concrete Product. 您可以将接口返回到产品而不是具体的产品。

Such as

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

And then verify the Owner property was not set: 然后验证未设置所有者属性:

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

If you care about all the properties and or methods, then there is probably a pre-existing way with Rhino. 如果您关心所有属性和/或方法,那么Rhino可能存在预先存在的方式。 Otherwise you can make an extension method that probably uses reflection such as: 否则你可以制作一个可能使用反射的扩展方法,例如:

Dep<IProduct>().AssertNoPropertyOrMethodWasCalled()

Our behaviour specifications are like so: 我们的行为规范是这样的:

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

Enjoy. 请享用。

If all consumers of ProductService.GetProduct() expect the same result as if they had asked it from the ProductRepository, why don't they just call ProductRepository.GetProduct() itself ? 如果ProductService.GetProduct()的所有使用者都期望得到与他们从ProductRepository中询问的结果相同的结果,为什么他们不只是调用ProductRepository.GetProduct()本身? It seems you have an unwanted Middle Man here. 看来你这里有一个不受欢迎的中间人

There's not much value added to ProductService.GetProduct(). ProductService.GetProduct()没有太多增值。 Dump it and have the client objects call ProductRepository.GetProduct() directly. 转储它并让客户端对象直接调用ProductRepository.GetProduct()。 Put the error handling and logging into ProductRepository.GetProduct() or the consumer code (possibly via AOP). 将错误处理和日志记录放入ProductRepository.GetProduct()或使用者代码(可能通过AOP)。

No more Middle Man, no more discrepancy problem, no more need to test for that discrepancy. 没有更多的中间人,没有更多的差异问题,不再需要测试这种差异。

Let me state the problem as I see it. 让我按照我的看法陈述问题。

  1. You have a method and a test method. 你有一个方法和一个测试方法。 The test method validates the original method. 测试方法验证原始方法。
  2. You change the system under test by altering the data. 您可以通过更改数据来更改受测系统。 What you want to see is that the same unit test fails. 你想看到的是同一个单元测试失败了。

So in effect you're creating a test that verifies that the data in the data source matches the data in your fetched object AFTER the service layer returns it. 因此,实际上您正在创建一个测试,用于验证数据源中的数据是否与服务层返回后提取的对象中的数据相匹配。 That probably falls under the class of "integration test." 这可能属于“集成测试”的范畴。

You don't have many good options in this case. 在这种情况下,你没有很多好的选择。 Ultimately, you want to know that every property is the same as some passed-in property value. 最终,您想要知道每个属性都与某些传入的属性值相同。 So you're forced to test each property independently. 所以你被迫独立测试每个属性。 You could do this with reflection, but that won't work well for nested collections. 您可以使用反射执行此操作,但这对嵌套集合不起作用。

I think the real question is: why test your service model for the correctness of your data layer, and why write code in your service model just to break the test? 我认为真正的问题是:为什么要测试您的服务模型以确保数据层的正确性,以及为什么在服务模型中编写代码只是为了打破测试? Are you concerned that you or other users might set objects to invalid states in your service layer? 您是否担心您或其他用户可能会将对象设置为服务层中的无效状态? In that case you should change your contract so that the Product.Owner is readonly . 在这种情况下,您应该更改合同,以便Product.Owner是readonly

You'd be better off writing a test against your data layer to ensure that it fetches data correctly, then use unit tests to check the business logic in your service layer. 您最好针对数据层编写测试以确保它正确地获取数据,然后使用单元测试来检查服务层中的业务逻辑。 If you're interested in more details about this approach reply in the comments. 如果您对评论中有关此方法的更多详细信息感兴趣。

Having look on all 4 hints provided it seems that you want to make an object immutable at runtime. 看看所有4个提示,似乎你想在运行时使对象不可变。 C# language does no support that. C#语言不支持。 It is possible only with refactoring the Product class itself. 只有重构Product类本身才有可能。 For refactoring you can take IReadonlyProduct approach and protect all setters from being called. 对于重构,您可以采用IReadonlyProduct方法并保护所有setter不被调用。 This however still allows modification of elements of containers like List<> being returned by getters. 但是,这仍然允许修改由getter返回的容器元素,如List<> ReadOnly collection won't help either. ReadOnly集合也无济于事。 Only WPF lets you change immutability at runtime with Freezable class. 只有WPF允许您在运行时使用Freezable类更改不变性。

So I see the only proper way to make sure objects have same contents is by comparing them. 所以我看到通过比较它们来确保对象具有相同内容的唯一正确方法。 Probably the easiest way would be to add [Serializable] attribute to all involved entities and do the serialization-with-comparison as suggested by Frank Schwieterman. 可能最简单的方法是将[Serializable]属性添加到所有涉及的实体,并按照Frank Schwieterman的建议进行序列化与比较。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM