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

Viewed 86

I am trying to mock the two interfaces below.

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

Here is the setup

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

Here is the controller action that is called.

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

This line returns null

var message = _emailTemplate.GetForgotPasswordTemplate(model);

Here is the method code

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

This line also returns null

await _emailSender.SendEmailAsync(sendEmailViewModel);

Here is the method code

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

Here is the test

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

However the test passes and what i am wondering is why do both of these methods return null and why does the test pass even though it returns null. How do i get these to return something?

One thing I do not understand is when to use It.Any and just pass in a normal object with some test data. How am i supposed to check a user exists if i use It.Any and i need to pass a model into my controller action?

1 Answers

Setup phase

Whenever you need to mock an interface method try be permissive during setup.
In other words allow to receive any parameter:

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

If your return value depends on the parameter
then use that overload of the Returns, which accepts a function:

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

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

Verification phase

During assertion try to be as specific as possible.
If you have access to the parameter then pass that on to the Verify:

var mockedViewTemplate = new EmailTemplateViewModel { ... };

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

Please bear in mind that moq uses reference check to determine that the expected and actual parameter are the same. If you don't have a reference to this parameter then you should use It.Is<T>:

const string expectedSubject = "ForgotPassword";

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

or if you wish to assert on more than one attribute then:

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