DEV Community

loading...
Cover image for How to write good unit tests? Have a failing test

How to write good unit tests? Have a failing test

canro91 profile image Cesar Aguirre Originally published at canro91.github.io Updated on ・4 min read

A passing test isn't always the only thing to look for. Having a failing test is important too. I learned this lesson the hard way. Let's see what happened.

To write reliable unit tests, always start writing a failing test. And, make sure it fails for the right reasons. Follow the Red, Green, Refactor principle of Test-Driven Development (TDD). Write a failing test, make it pass and refactor the code. Don't skip the failing test part.

The Passing test

Let's continue with the same example from our previous post on how to write good unit tests.

From our last example, we had a controller to create, update and suspend user accounts. Inside its constructor, this controller validated some email addresses from an injected configuration object. We wrote a test like this:

[TestMethod]
public void AccountController_SenderEmailIsNull_ThrowsException()
{
    var emailConfig = Options.Create(new EmailConfiguration
    {
        SenderEmail = null,
        ReplyToEmail = "email@email.com",
        SupportEmail = "email@email.com"
    });

    Assert.ThrowsException<ArgumentNullException>(() =>
        CreateAccountController(emailConfig));
}

private AccountController MakeAccountController(IOptions<EmailConfiguration> emailConfiguration)
{
    var mapper = new Mock<IMapper>();
    var logger = new Mock<ILogger<AccountController>>();
    var accountService = new Mock<IAccountService>();
    var accountPersonService = new Mock<IAccountPersonService>();
    var emailService = new Mock<IEmailService>();
    var emailConfig = new Mock<IOptions<EmailConfiguration>>();
    var httpContextAccessor = new Mock<IHttpContextAccessor>();

    return new AccountController(
            mapper.Object,
            logger.Object,
            accountService.Object,
            accountPersonService.Object,
            emailService.Object,
            emailConfiguration,
            httpContextAccessor.Object);
}
Enter fullscreen mode Exit fullscreen mode

Always start writing a failing test

Always start writing a failing test. Photo by Neora Aylon on Unsplash

A false positive

This time, I had a new requirement. I needed to add a new method to our AccountController. This new method reads another configuration object injected into the controller.

To follow the convention of validating required parameters inside constructors, I also check for this new configuration object. I wrote a new test and a builder method to call the constructor with only the parameters I need.

[TestMethod]
public void AccountController_NoNewConfig_ThrowsException()
{
    var options = Options.Create<SomeNewConfig>(null);

    Assert.ThrowsException<ArgumentNullException>(() => CreateAccountController(options));
}

private AccountController CreateAccountController(IOptions<SomeNewConfig> someNewConfig)
{
    var emailConfig = new Mock<IOptions<EmailConfiguration>());
    return CreateAccountController(emailConfig.Object, someNewConfig);
}

private AccountController MakeAccountController(IOptions<EmailConfiguration> emailConfig, IOptions<SomeNewConfig> someNewConfig)
{
    // It calls the constructor with mocks, except for emailConfig and someNewConfig
}
Enter fullscreen mode Exit fullscreen mode

And, the constructor looked like this:

public class AccountController : Controller
{
  public AccountController(
      IMapper mapper,
      ILogger<AccountController> logger,
      IAccountService accountService,
      IAccountPersonService accountPersonService,
      IEmailService emailService,
      IOptions<EmailConfiguration> emailConfig,
      IHttpContextAccessor httpContextAccessor,
      IOptions<SomeNewConfig> someNewConfig)
  {
      var emailConfiguration = emailConfig?.Value ?? throw new ArgumentNullException($"EmailConfiguration");
      if (string.IsNullOrEmpty(emailConfiguration.SenderEmail))
      {
          throw new ArgumentNullException($"SenderEmail");
      }

      var someNewConfiguration = someNewConfig?.Value ?? throw new ArgumentNullException($"SomeNewConfig");
      if (string.IsNullOrEmpty(someNewConfiguration.SomeKey)
      {
          throw new ArgumentNullException($"SomeKey");
      }

      // etc...
  }
}
Enter fullscreen mode Exit fullscreen mode

I ran the test and it passed. Move on! But...Wait! There's something wrong! Did you spot the issue?

Make your tests fail

Of course, that test is passing. The code throws an ArgumentNullException. But, that exception is coming from the wrong place. It comes from the validation for the email configuration, not from our new validation. I forgot to use a valid email configuration in the new builder method. I used a mock reference instead. I only realized that after getting my code reviewed. Point for the code review!

Make sure to always start writing a failing test. And, this test should fail for the right reasons. If you write your tests after writing the production code, comment some parts of your production code to see your tests failing. Or change the assertions on purpose.

When you make a failed test pass, you're testing the test. You're making sure it fails and passes when it should. You know you aren't writing buggy tests or introducing false positives into your test suite.

A better test for our example would check the exception message. Like this:

[TestMethod]
public void AccountController_NoSomeNewConfig_ThrowsException()
{
    var options = Options.Create<SomeNewConfig>(null);

    var ex = Assert.ThrowsException<ArgumentNullException>(() => CreateAccountController(options));
    StringAssert.Contains(ex.Message, nameof(SomeNewConfig));
}
Enter fullscreen mode Exit fullscreen mode

Voilà! This task reminded me to always see my tests failing for the right reasons. Do you have passing tests? Do they pass and fail when they should? I hope they do after reading this post.

For more tips on unit testing, check my takeaways from the book The Art of Unit Testing and my post on how to write fakes with Moq.

Happy unit testing!

Discussion (0)

pic
Editor guide