Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/unit-testing/4.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 单元测试,模拟-简单案例:服务-存储库_C#_Unit Testing_Service_Mocking_Repository - Fatal编程技术网

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

C# 单元测试,模拟-简单案例:服务-存储库,c#,unit-testing,service,mocking,repository,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); }

考虑以下服务块:

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是否返回了与从存储库返回的对象相同的对象。

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

问题2:您不需要在
产品
网关中进行更改来更改产品的所有者。您可以在模型中进行更改

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

你也可以使用mock。为什么要测试实现细节?网关只关心
\u 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();
}
[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);
}

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

就个人而言,我不在乎这一点。测试应该确保代码执行了您想要的操作很难测试代码没有做什么,在这种情况下,我不想麻烦

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

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

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

好吧,一种方法是传递产品的模拟,而不是实际的产品。通过严格控制产品,确保不影响产品。(我假设您正在使用最小起订量,看起来您正在使用)

[测试]
public void GetProduct\u return\u与\u productRepository()上的\u GetProduct\u相同的\u产品\u{
var product=新模拟(MockBehavior.Strict);
_productRepositoryMock.Setup(pr=>pr.GetProduct(product.Id)).Returns(product);
返回的产品Product=\u productService.GetProduct(Product.Id);
断言.AreEqual(产品、退货产品);
_productRepositoryMock.VerifyAll();
product.VerifyAll();
}

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

我不确定单元测试是否应该关注“给定的方法做了什么而不是”。有无数的步骤是可能的。严格来说,测试“GetProduct(id)返回与productRepository上的GetProduct(id)相同的产品”是正确的,无论有无行
product.Owner=“TotallyDifferentTowner”

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


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

单元测试的一种思维方式是编码规范。当您使用
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
(更改是我们从新创建的产品中检索所有者,并从服务返回的产品中检查所有者。)

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

我经常通过规定“如果我更改这行代码,调整一两个关键常量,或者注入一些代码错误(例如,将!=更改为==),哪个测试将捕获错误?”来“测试我的测试”,这样做可以真正发现是否有捕获问题的测试。有时不是,在这种情况下,是时候看看测试中隐含的需求了,看看我们如何收紧它们。在没有实际需求捕获/分析的项目中,这是一个有用的工具,可以强化测试,以便在发生意外更改时失败

当然,你必须务实。你不能合理地期望处理所有的变化——有些变化简直是荒谬的,而且
Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg.Is.Anything);
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);
   }
}
// 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();
}
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;
        });
    }
}