最小起订量为什么模拟方法 return 为 null 并测试直到通过?

Moq why do mocked methods return null and tests till pass?

我正在尝试模拟下面的两个接口。

Mock<IEmailSender> emailSender = new Mock<IEmailSender>();
Mock<IEmailTemplate> emailTemplate = new Mock<IEmailTemplate>();

这是设置

emailTemplate.Setup(x => x.GetForgotPasswordTemplate(It.IsAny<EmailTemplateViewModel>())).Returns(It.IsAny<string>());
emailSender.Setup(x => x.SendEmailAsync(It.IsAny<SendEmailViewModel>(), default)).ReturnsAsync(It.IsAny<SendEmailResultViewModel>());

这是调用的控制器操作。

[EnableCors(PolicyName = "AllowClientAccess")]
[HttpGet("Forgot")]
public async Task<IActionResult> ForgotPassword([FromQuery] string email)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user != null)
    {
        //MOQ file path not found
        EmailTemplateViewModel model = new EmailTemplateViewModel();
        model.Email = email;
        model.RecipientName = user.UserName;

        var message = _emailTemplate.GetForgotPasswordTemplate(model);

        SendEmailViewModel sendEmailViewModel = new SendEmailViewModel();

        sendEmailViewModel.RecipientName = user.UserName;
        sendEmailViewModel.RecipientEmail = user.Email;
        sendEmailViewModel.Subject = "ForgotPassword";
        sendEmailViewModel.Body = message;

        await _emailSender.SendEmailAsync(sendEmailViewModel);
        return Ok(AddSuccess("Check your email", "Forgot Password"));
    }

    ModelState.AddModelError("Forgot Password","Unable to send email");
    return BadRequest(ModelErrors());
}

这一行return为空

var message = _emailTemplate.GetForgotPasswordTemplate(model);

这里是方法代码

public string GetForgotPasswordTemplate(EmailTemplateViewModel model)
{
    try
    {
        var utcNow = DateTime.Now;
        if (_testEmailTemplate == null)
            if (File.Exists("Helpers/Templates/ForgotPasswordEmail.template"))
                _testEmailTemplate = ReadPhysicalFile("Helpers/Templates/ForgotPasswordEmail.template");

        var appUrl = _configuration.GetSection("ApplicationUrl").Value +
                     "/reset-password?&email=" + model.Email;
        var emailMessage = _testEmailTemplate
            .Replace("{user}", model.RecipientName)
            .Replace("{testDate}", utcNow.ToString(CultureInfo.InvariantCulture))
            .Replace("{appUrl}", appUrl);

        return emailMessage;
    }
    catch (Exception e)
    {
        Log.Warning(e, "Email error");
        throw;
    }
}

这一行也return为空

await _emailSender.SendEmailAsync(sendEmailViewModel);

这里是方法代码

public Task<SendEmailResultViewModel> SendEmailAsync(SendEmailViewModel model, SmtpConfig config = default)
{
    model.IsHtml = true;
    
    var from = new MailboxAddress(_config.FromName, _config.FromEmail);
    var to = new MailboxAddress(model.RecipientName, model.RecipientEmail);

    return SendEmailAsync(@from, new[] {to}, model.Body, model.Body, config, model.IsHtml);
}

这是测试

[Theory]
[InlineData("stephen@kaizenappz.com")]
public async Task WhenAUserForgetsPasswordAHttpStatusCode200ShouldBeReturnedAsync(string email)
{
    var confirmUser = await Controller.ForgotPassword(email);

    var result = confirmUser as OkObjectResult;

    var actual = (HttpStatusCode)result?.StatusCode.Value;
    var expected = HttpStatusCode.OK;

    Assert.AreEqual(expected, actual);
}

但是测试通过了,我想知道的是为什么这两种方法 return 都为空,为什么测试通过,即使它 return 为空。我怎样才能得到这些 return 东西?

我不明白的一件事是什么时候使用 It.Any 并且只是传入一个带有一些测试数据的普通对象。如果我使用 It.Any 并且我需要将模型传递到我的控制器操作中,我应该如何检查用户是否存在?

设置阶段

每当您需要模拟接口方法时,请尝试在设置过程中保持宽容。
换句话说允许接收任何参数:

const string mockedForgotPwdTemplate = "...";
emailTemplate
  .Setup(template => template.GetForgotPasswordTemplate(It.IsAny<EmailTemplateViewModel>()))
  .Returns(mockedForgotPwdTemplate);

如果你的return值取决于参数
然后使用 Returns 的重载,它接受一个函数:

const string mockedTemplateWithSubject = "..."; 
const string mockedTemplateWithoutSubject = "...";

emailTemplate
  .Setup(template => template.GetForgotPasswordTemplate(It.IsAny<EmailTemplateViewModel>()))
  .Returns((EmailTemplateViewModel vm) => !string.IsNullOrEmpty(vm.Subject) ? mockedTemplateWithSubject : mockedTemplateWithoutSubject);

验证阶段

在断言期间尽量具体。
如果您有权访问该参数,则将其传递给 Verify:

var mockedViewTemplate = new EmailTemplateViewModel { ... };

emailTemplate
  .Verify(template => template.GetForgotPasswordTemplate(mockedViewTemplate), Times.Once);

请记住,moq 使用引用检查来确定 expectedactual 参数是否相同。如果您没有对此参数的引用,那么您应该使用 It.Is<T>:

const string expectedSubject = "ForgotPassword";

emailTemplate
  .Verify(template => template.GetForgotPasswordTemplate(
     It.Is<EmailTemplateViewModel>(vm => expectedSubject == vm.Subject), Times.Once);

或者如果您希望对多个属性进行断言,则:

private bool AssertViewModel(EmailTemplateViewModel actualVM, string expectedSubject, string expectedRecipientName)
{
   Assert.Equal(expectedSubject, actualVM.Subject);
   Assert.Equal(expectedRecipientName, actualVM.RecipientName);

   return true;
}  

//...

const string expectedSubject = "ForgotPassword", expectedRecipent = "...";

emailTemplate
  .Verify(template => template.GetForgotPasswordTemplate(
     It.Is<EmailTemplateViewModel>(vm => this.AssertViewModel(vm, expectedSubject, expectedRecipient)), Times.Once);