【问题标题】:How to Mock a Service with Moq and Nunit in mvc application - prevent nulls如何在 mvc 应用程序中使用 Moq 和 Nunit 模拟服务 - 防止空值
【发布时间】:2016-12-01 01:11:16
【问题描述】:

我的印象是模拟是伪造数据调用,因此没有什么是真实的。因此,当我尝试创建与团队中其他开发人员所做的非常相似的自己的单元测试时,我认为更新服务是不正确的。

    [Test]
    public void TemplateService_UpdateTemplate_ContentNameNotUnique_ServiceReturnsError()
    {
        Template Template = new Template()
        {
            TemplateId = 123,
        };

        // Arrange.
        TemplateUpdateRequest request = new TemplateUpdateRequest()
        {
            ExistingTemplate = Template
        };

        var templateRepo = new Mock<ITemplateRepository>();
        var uproduceRepo = new Mock<IUProduceRepository>();


            templateRepo.Setup(p => p.IsUniqueTemplateItemName(215, 456, "Content", It.IsAny<string>())).Returns(false);
            templateRepo.Setup(p => p.UpdateTemplate(request)).Returns(1);

            // Act.
            TemplateService svc = new TemplateService(templateRepo.Object, uproduceRepo.Object);

            TemplateResponse response = svc.UpdateTemplate(request);

            // Assert.
            Assert.IsNotNull(response);
            Assert.IsNotNull(response.data);
            Assert.IsNull(response.Error);

    }

所以我的问题在于这段代码:

TemplateService svc = new TemplateService(templateRepo.Object, uproduceRepo.Object);

TemplateService 真的应该更新吗? “如果”服务最终命中数据库和/或文件系统会怎样?那么它就变成了集成测试,不再是单元测试了,对吧?

TemplateResponse response = svc.UpdateTemplate(request);

另外,我如何真正控制这是否真的会通过?是依赖一个我没有写的服务调用,如果有bug,或者遇到问题怎么办,或者返回NULL,这正是我不想要的! ?

【问题讨论】:

    标签: c# asp.net-mvc unit-testing nunit moq


    【解决方案1】:

    单元测试旨在测试一个功能单元,它通常是(但绝对不必是)单个类(因为我们应该遵循单一职责原则)。因此,可以新建您尝试测试的类 - 这是您需要模拟/存根的依赖项,在您的情况下,这就是您对两个存储库所做的事情。

    您是否来电

    TemplateResponse response = svc.UpdateTemplate(request);
    

    最终命中数据库是正确使用依赖注入的问题 - 您的团队需要在使用显式依赖项(即构造函数注入的存储库)方面受到纪律处分,而不是在类中更新存储库。

    关于你的测试是否会通过——如果类只有显式依赖(在你的测试中被模拟出来),那么只有调用中的逻辑会被测试,这正是你想要的!

    【讨论】:

      【解决方案2】:

      如果您正在测试 TemplateService,那么 YES 应该更新它。你还能如何测试你的实际实现?这里的重点是测试TemplateService,而不是它的依赖项(除非它是一个集成测试)。

      因此,如果您有像 IUProduceRepository 这样的存储库,则应该对其进行模拟,以确保您没有写入某些数据库,并确保您可以创建特定的场景。

      您的目标是根据特定场景测试TemplateService 是否在做正确的事情。因此,假设您的 IUProduceRepository 抛出了您的 TemplateService 应该处理的错误。然后,您应该在 IUProduceRepository 中模拟该错误并测试 TemplateService实现,并确保它按预期处理它。

      所以回答你的问题...

      TemplateService 真的应该更新吗?什么“如果”服务 最终击中数据库和/或文件系统?然后它变成了一个 集成测试,不再是单元测试,对吧?

      是的,我会说这将是一个集成测试。你模拟一些东西以确保服务没有写入数据库。

      另外,我如何真正控制这是否真的会通过或 不是?它依赖于我没有编写的服务调用,那么如果 有bug,或者遇到问题,或者返​​回NULL,就是 正是我不想要的! ?

      您的目标是确保您测试的课程在特定场景中符合您的预期。假设您的 repo 出于某种原因引发了异常,那么您可能想要测试您的服务是否可以处理该异常(或至少测试它是否正在执行预期的操作)。

      public class TemplateService : ITemplateService
      {
      
          ITemplateRepository _templateRepo; 
          IUProduceRepository _produceRepo
      
          public TemplateService(ITemplateRepository templateRepo, IUProduceRepository produceRepo)
          {
              _templateRepo = templateRepo;
              _produceRepo = produceRepo;
          } 
      
      
          public TemplateResponse UpdateTemplate(Template template)
          {
              try
              {
                  var result = _templateRepo.Update(template);
                  return result;
              }
              catch(Exception ex)
              {
                  // If something went wrong. Always return null. (or whatever)
                  return null;
              }
          }
      }
      
      [Test]
      public void TemplateService_UpdateTemplate_ShouldReturnNullOnError()
      {
          Template template = new Template() // note that I changed the variable name.
          {
              TemplateId = 123,
          };
      
          // Arrange.
          TemplateUpdateRequest request = new TemplateUpdateRequest()
          {
              ExistingTemplate = template 
          };
      
          var templateRepo = new Mock<ITemplateRepository>();
          var uproduceRepo = new Mock<IUProduceRepository>();
      
      
           // Mock exception
           templateRepo.Setup(p => p.UpdateTemplate(request)).Throw(new ArgumentException("Something went wrong");
      
           // Act.
           TemplateService svc = new TemplateService(templateRepo, uproduceRepo);
      
           TemplateResponse response = svc.UpdateTemplate(request);
      
           // Assert.
           // Make sure that the exception is handled and null is returned instead.
           Assert.IsNull(response); 
      }
      

      您在上述案例中实际测试的ONLY 是,如果您的存储库中有错误,您的服务将返回null。这样,您就为特定场景设计了一个测试,并确保您的服务在该场景发生时执行预期的操作。

      我知道您特别提到您不希望它返回 null,但这更多是我在谈论的概念。所以假设 repo 返回null... 为它写一个测试。模拟 repo 以返回 null,然后测试您的服务是否正在执行它应该执行的操作(日志记录等)。

      那么如果有bug,或者遇到问题,或者返​​回NULL, 这正是我不想要的! ?

      这就是单元测试的全部意义所在。找到一个场景,然后确保您正在测试的课程正在做它的一部分。模拟您的依赖项以创建特定场景。

      当我编写测试时,我有很多测试只是断言调用了一个方法。假设我有一个服务,唯一的职责就是调用 repo。然后你可能想写三个测试

      1. 测试服务是否实际调用了 repo。
      2. 使用存储库中的值测试服务是否按照预期执行。
      3. 测试如果 repo 抛出异常会发生什么。

      【讨论】:

        【解决方案3】:

        交互测试是单元测试的一种形式,您可以在其中为所有事物(或某些事物,或仅像数据库或磁盘之类的真正昂贵的事物)提供 虚假 依赖项,对此有很多不同的解释) 除了你真正想要测试的东西。

        在您的示例中,您正在测试 TemplateService 代码的行为是否正确。该测试提供了虚假的协作者(存储库),您可以对其进行设置,以便它们在不同的测试中返回不同的东西。这样,您就可以验证 TemplateService 在这些情况下的行为是否正确。

        如果您对新建 TemplateService 的担忧是在 TemplateService 代码本身中可能存在对真实数据库的一些剩余调用,那是 TemplateService 代码的设计问题。这些调用不应发生在 TemplateService 中,而应在协作对象上实现(正是为了让您可以在针对您的服务的集中隔离测试中伪造它们)。

        编写单元测试而不更新被测系统会提供零值。如果每个对象都是假的,你如何测试你的实际生产代码?

        关于这些模拟服务是如何实际实现的(它们会抛出空值吗?有任何错误吗?它们是否返回空值?),这是一个您可以通过

        解决的问题

        a) 编写集成测试(警告:这不能很好地扩展,请参阅有关为什么集成测试是骗局的相关阅读)。

        或 b) 使用 J.B. Rainsberger 所谓的“合同测试”(参见下面的相关阅读材料)来验证实际的合作者对象是否真的按照消费者期望的方式行事。

        相关阅读:

        【讨论】:

        • 我同意你的回答,但在我的旅行中我不同意“综合测试是骗局”在那篇文章中,我大多同意“亚历克斯”与作者辩论的观点。即使我的团队中有 Kent Beck 负责单元测试,我个人仍然想要集成测试。
        【解决方案4】:

        我建议您阅读本书The Art of Unit Testing: with examples in C# 以学习良好的做法。

        在您的示例中,您正在测试 TemplateService 类。 你关心的是如果TemplateService 调用数据库怎么办。这取决于这个类是如何实现的。从示例和模拟设置中,我可以理解ITemplateRepository 的详细信息负责数据库调用,这就是模拟UpdateTemplateIsUniqueTemplateItemName 的原因。

        如果你想避免Null检查,那么你可以检查svc.UpdateTemplate(request)是否使用其参数调用ITemplateRepository的方法UpdateTemplate

        应该类似如下

        templateRepo.Verify(u => u.UpdateTemplate(It.Is<TemplateUpdateRequest>(r => r.ExistingTemplate.TemplateId == 123)),Times.Once);
        

        您可以验证您已模拟的其他方法调用。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-07-31
          • 2012-10-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-10-15
          • 2010-11-29
          • 1970-01-01
          相关资源
          最近更新 更多