Unit testing should be an essential part of your SDLC, especially when you have multiple contributors to a project. It’s a great way to ensure that your code is still returning the appropriate responses, even after someone else (who might have a different context than you) has tinkered with it.

Don’t get me wrong, writing unit tests can be an exercise in tedium but it has saved my behind more times than I care to admit. There are tools to make things a bit easier though and I’ll go through a couple of them over the next few posts and end with a bit of a secret weapon (hint: it’s ChatGPT).

In this post, we’ll talk about the initial setup and getting our first unit tests going.

Sample Project

I’m going to use the base ASP.NET Web API project to start things off. It’s a simple project that I’m going to bloat purely for the purposes of this demonstration.

You can follow along by running dotnet new webapi or simply pull the completed project from my public git repo:

https://github.com/Wallyza/EasyTests/tree/v1.0

Our Controller

In our WeatherForecastController we’ll find a simple GET that looks as below:

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
    }

This is pretty much how most weather forecasting works, they just use much more powerful computers to get the same job done, really.

I’m going to overcomplicate this controller a little bit by extracting the code in to it’s own service and inject that service into our DI container.

Step 1

Create a new file called WeatherForecastService.cs and make it look like the below code:

// WeatherForecastService.cs
namespace EasyTests.Services;

public interface IWeatherForecastService
{
	IEnumerable<WeatherForecast> GetWeatherForecast();
}

public class WeatherForecastService : IWeatherForecastService
{
	private static readonly string[] Summaries = new[]
	{
		"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
	};

	public IEnumerable<WeatherForecast> GetWeatherForecast()
	{
		return Enumerable.Range(1, 5).Select(index => new WeatherForecast
		{
			Date = DateTime.Now.AddDays(index),
			TemperatureC = Random.Shared.Next(-20, 55),
			Summary = Summaries[Random.Shared.Next(Summaries.Length)]
		})
		.ToArray();
	}
}

Step 2

Then we add this to our Dependency Injection Container by adding the following line to Program.cs:

builder.Services.AddTransient<IWeatherForecastService, WeatherForecastService>();

Step 3

Next up, we inject that service into WeatherForecastController.cs by changing the code as follows:

using EasyTests.Services;
using Microsoft.AspNetCore.Mvc;

namespace EasyTests.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IWeatherForecastService _forecastService;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherForecastService forecastService)
    {
        _logger = logger;
        _forecastService = forecastService;
    }
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return _forecastService.GetWeatherForecast();
    }

}

Finally, we can test this by running dotnet run and directing our browser to the appropriate URL (https://localhost:7104/WeatherForecast) to see the response.

On to the testing

I’ll admit, that was a lot of effort to overcomplicate something incredibly simple but there is method behind the madness, I’ll get to that later. First, let’s write a unit test.

xUnit

I prefer to use xUnit over nUnit. It’s a personal preference, I think it overcomes some of the limitations of nUnit but others might disagree. Use whatever you want! But in this project, I’m adding a test project called EasyTests.Tests.

In there, we’ll create our tests for the WeatherForecastService called WeatherForecastServiceTests, creative, eh?

using Xunit;

namespace EasyTests.Tests.Services;

public class WeatherForecastServiceTests
{
    [Fact]
    public void GetWeatherForecast_None_Returns5()
    {
        // arrange

        // act

        // assert
    }
}

So we have a class that tests another class, and the method is decorated with a [Fact] attribute to indicate to the test runner that this is a…fact. Based on the name of our method we can tell which method we’re testing and what result we’re expecting (that’s just for us).

Before I continue, let’s talk a bit about

Unit Test Composition

When I name unit tests I like to stick with a specific naming schema:

  • The method you’re testing: GetWeatherForecast
  • Parameters (if any): None
  • and finally Expected Outcome Returns5

Further, inside the method we have 3 sections namely arrange, act, and assert. Let’s describe each below:

  • Arrange is where we prepare things like test data and parameters, and create instances of test classes.
  • Act is where we actually fire off the method we are testing. We either store the result or capture it in a function to check for exceptions (we’ll talk about exception testing in a later post).
  • Assert is where we take the response gathered in the Act phase and we assert that it is what we were expecting.

Back to our unit test

Let’s fill out our test with what we’re testing:

    [Fact]
    public void GetWeatherForecast_None_Returns5()
    {
        // arrange
        var testClass = new WeatherForecastService();
        // act
        var response = testClass.GetWeatherForecast();
        // assert
        Assert.Equal(5, response.Count());
    }

Really simple and straightforward. There is one more thing we can do to make it even more simple. One of my favourite libraries is the FluentAssertions package that you can (and I recommend you do) grab from Nuget.

Using that we can change our assertion to look a bit nicer, like this:

    [Fact]
    public void GetWeatherForecast_None_Returns5()
    {
        // arrange
        var testClass = new WeatherForecastService();
        // act
        var response = testClass.GetWeatherForecast();
        // assert
        response.Should().HaveCount(5);
    }

To me, that is a lot easier to read.

Let’s fill out some more unit tests and round out our first run of unit tests. When we’re done, the test class should look like this:

public class WeatherForecastServiceTests
{
    private readonly WeatherForecastService _testClass = new WeatherForecastService();

    [Fact]
    public void GetWeatherForecast_None_Returns5()
    {
        // arrange

        // act
        var response = _testClass.GetWeatherForecast();
        // assert
        response.Should().HaveCount(5);
    }

    [Fact]
    public void GetWeatherForecast_None_NotNull()
    {
        // arrange

        // act
        var response = _testClass.GetWeatherForecast();
        // assert
        response.Should().NotBeNull();
    }

    [Fact]
    public void GetWeatherForecast_None_RespectLimits()
    {
        // arrange

        // act
        var response = _testClass.GetWeatherForecast();
        // assert
        response.Max(x => x.TemperatureC).Should().BeInRange(-20, 55);
    }
}

Notice how I moved the arrange bit to a private field.

Conclusion

Getting unit tests up and running is very easy and even though these seem like mundane tests, they’re ensuring a contract is maintained and responses remain consistent. Whether it’s you or a fellow coder adding to the code.

In my next post, I’m going to overcomplicate things even more and look at using Moq to create mock classes that our unit tests can rely on to ensure we’re testing in isolation (they’re unit tests after all) and our tests don’t change if underlying data changes.

If you’ve stuck around this far because you’re curious why I moved the code out of the controller in the first place. I’m a firm believer that the backend and it’s interfaces should retain its separation of concerns. Controllers return a response, that is all. As such it has become a pet peeve of mine.