如何在 mvc 应用程序中使用 Moq 和 Nunit 模拟服务 - 防止空值

How to Mock a Service with Moq and Nunit in mvc application - prevent nulls

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

    [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真的要更新吗?什么 "if" 服务最终命中了数据库 and/or 文件系统?然后就变成集成测试了,不再是单元测试了吧?

TemplateResponse response = svc.UpdateTemplate(request);

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

单元测试旨在测试功能单元,通常是(但绝对不需要)单个 class(因为我们应该遵循单一职责原则)。因此,可以更新您要测试的 class(es) - 这是您需要 mock/stub 的依赖项,在您的情况下,这就是您对两个存储库。

是否来电

TemplateResponse response = svc.UpdateTemplate(request);

最终命中数据库是正确使用依赖注入的问题——你的团队需要在使用显式依赖项(即构造函数注入的存储库)方面受到纪律约束,而不是在 [=20] 内更新存储库=].

并且关于您的测试是否会通过 - 如果 class 仅具有显式依赖项(在您的测试中被模拟),则只会测试调用中的逻辑,这正是您想要的!

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

所以如果你有一个像 IUProduceRepository 这样的 repo,它应该被模拟,只是为了确保你没有写入某些数据库,并确保你可以创建一个特定的场景。

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

所以回答你的问题...

Should the TemplateService really be newed up? What "if" the Service ended up hitting a database and/or file system? Then it becomes an integration test, and no longer a unit test, right?

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

Also, how do I really control whether this is truly going to pass or not? It is relying on a service call that I didn't write, so what if there is a bug, or encounters a problem, or return NULL, which is exactly what I do not want! ?

您的目标是确保您测试的 class 在特定场景中执行您期望的操作。假设您的存储库由于某种原因抛出异常,那么您可能想要测试您的服务是否可以处理该异常(或者至少测试它是否按照预期进行)。

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

你在上面的例子中实际测试的唯一是你的服务将returnnull如果你的回购中有错误。这样,您就为特定场景设计了测试,并确保您的服务在该场景发生时按预期运行。

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

so what if there is a bug, or encounters a problem, or return NULL, which is exactly what I do not want! ?

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

当我编写测试时,我有很多测试只是断言调用了一个方法。假设我有一项服务,唯一的责任就是调用回购协议。那么你可能想写三个测试

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

交互测试是单元测试的一种形式,您可以在其中为所有内容(或某些内容,或仅提供真正昂贵的内容,如数据库或磁盘,有很多关于这个的不同解释)除了你真正想要测试的东西。

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

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

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

关于您关于这些模拟服务的实际实现方式的观点(它们会抛出空值吗?有错误吗?它们 return NULL 吗?),这是一个您可以通过

解决的问题

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

或 b) 使用某些东西 J.B。 Rainsberger 调用 "Contract tests"(请参阅下面的相关阅读)来验证实际协作对象的行为是否确实按照消费者期望的方式进行。

相关阅读:

我建议你阅读这本书 The Art of Unit Testing: with examples in C# 以学习良好的实践。

在您的示例中,您正在测试 TemplateService class。 您关心的是如果 TemplateService 调用数据库会怎样。这取决于这个 class 是如何实现的。从示例和模拟设置中,我可以理解 ITemplateRepository 的细节负责数据库调用,这就是为什么 UpdateTemplateIsUniqueTemplateItemName 被模拟的原因。

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

应该类似如下

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

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