Photo by Roman Mager

Introduction

This is the first post in the series, Testing your dotnet applications.
I will show you how I test my applications and libraries, both ASP.NET and dotnet core (console) applications. I will focus mainly on integration tests. When I say integration tests, I have the following definition in mind:

Integration testing is the phase in software testing in which individual software modules are combined and tested as a group.

In an ASP.NET Core project for example, I would test the "whole chain" by sending a request and then assert on the response. This includes using a real database and stuff like that.

In this series we will have one ASP.NET Core application called MyWeatherApp API. We will also have other types of applications (console applications etc). This post will focus on the API.

Right now we only have one endpoint, /weatherForecast. It returns hardcoded forecasts.

[HttpGet]
public IEnumerable<WeatherForecast> List()
{
    return new List<WeatherForecast>
    {
        new() { Date = DateOnly.Parse("2022-10-17"), Summary = "Freezing", TemperatureCelsius = 12 },
        new() { Date = DateOnly.Parse("2022-10-18"), Summary = "Bracing", TemperatureCelsius = 13 },
        new() { Date = DateOnly.Parse("2022-10-19"), Summary = "Chilly", TemperatureCelsius = 14 },
        new() { Date = DateOnly.Parse("2022-10-20"), Summary = "Cool", TemperatureCelsius = 14 },
        new() { Date = DateOnly.Parse("2022-10-21"), Summary = "Mild", TemperatureCelsius = 14 }
    };
}

Getting started

We will add some integration tests to our MyWeatherApp API.
All code can be found over at GitHub. The link points to a specific tag so you will only see stuff that I've talked about so far, no spoilers! ๐Ÿ˜€

JOS.Tests

JOS.Tests is a couple of projects that will evolve throughout this series. Right now they don't provide much value, just know that we have added a reference to the JOS.Tests.Integration.AspNet project in our test project. Later in this series, the JOS.Tests projects will be extended and provide more value.

Test project

We have created a project called MyWeatherApp.Api.Tests. This project will contain all tests for our API.

We have a couple of interesting files, let's have a look:

MyWeatherAppApiFixture

public class MyWeatherAppApiFixture : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        var testConfiguration = new MyWeatherAppApiTestConfiguration();
        builder
            .UseEnvironment(testConfiguration["ASPNETCORE_ENVIRONMENT"])
            .ConfigureAppConfiguration(configurationBuilder =>
            {
                configurationBuilder.AddInMemoryCollection(testConfiguration!);
            });
    }
}

Maybe not that exciting, at least for now, but this file will evolve througout this series. For now, the only thing you need to know is that this file sets the Environment to whatever we have specified in the MyWeatherAppApiTestConfiguration file.

MyWeatherAppApiTestConfiguration

public class MyWeatherAppApiTestConfiguration : TestConfiguration
{
    public MyWeatherAppApiTestConfiguration()
    {
        this["ASPNETCORE_ENVIRONMENT"] = "TestRunner";
    }
}

Right now, we are only specifying a custom environment in this file. We will later configure test specific connectionstrings etc in this file.

WeatherForecastTests
This is the file that actually contains our integration tests. Right now, we have one test that asserts that we get a 200 OK back from our /weatherforecast endpoint. You can see that we inject the MyWeatherAppApiFixture here. This allows us to create a HttpClient by using the CreateClient method. This method is available to us thanks to the WebApplicationFactory class that we inherit. We will focus more on the WebApplicationFactory later in this series.

[Collection(IntegrationTestCollection.Name)]
public class WeatherForecastTests : IClassFixture<MyWeatherAppApiFixture>
{
    private readonly MyWeatherAppApiFixture _fixture;

    public WeatherForecastTests(MyWeatherAppApiFixture fixture)
    {
        _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
    }
    
    [Fact]
    public async Task GET_WeatherForecast_ShouldReturn200Ok()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast");
        var client = _fixture.CreateClient();

        using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        
        response.StatusCode.ShouldBe(HttpStatusCode.OK);
    }
}

We can add another test that verifies that the data returned in the response body is what we expect as well:

[Fact]
public async Task GET_WeatherForecast_ShouldReturnExpectedForecast()
{
    var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast");
    var client = _fixture.CreateClient();

    using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

    response.EnsureSuccessStatusCode();
    var responseContent = await response.Content.ReadAsStreamAsync();
    var jsonResponse = await JsonDocument.ParseAsync(responseContent);
    var weatherForecastResponse = jsonResponse.RootElement.EnumerateArray();
    weatherForecastResponse.Count().ShouldBe(5);
    AssertWeatherForecast(weatherForecastResponse.ElementAt(0), "2022-10-17", "Freezing", 12);
    AssertWeatherForecast(weatherForecastResponse.ElementAt(1), "2022-10-18", "Bracing", 13);
    AssertWeatherForecast(weatherForecastResponse.ElementAt(2), "2022-10-19", "Chilly", 14);
    AssertWeatherForecast(weatherForecastResponse.ElementAt(3), "2022-10-20", "Cool", 14);
    AssertWeatherForecast(weatherForecastResponse.ElementAt(4), "2022-10-21", "Mild", 14);
}

private static void AssertWeatherForecast(JsonElement weatherForecast, string date, string summary, int temperatureC)
    {
        weatherForecast.GetProperty("date").GetString().ShouldBe(date);
        weatherForecast.GetProperty("summary").GetString().ShouldBe(summary);
        weatherForecast.GetProperty("temperatureCelsius").GetInt32().ShouldBe(temperatureC);
    }

That's all for this post, feel free to checkout the code and follow along!

All posts in this series can be found here.)