Testing ASP.NET Core endpoints that use Entity Framework Core

6 minute read

In the previous post, we saw how to create NUnit test projects that use libraries like Testcontainers and Respawn to write unit tests targeting components that directly access a database via Entity Framework Core.

In this post, we’ll look at how we can use the same libraries and the WebApplicationFactory utility to write end-to-end tests targeting ASP.NET Core endpoints using Entity Framework Core.

I already wrote about the WebApplicationFactory in a previous post.

Sample Project Overview

In this post, we will continue building on the sample project used in the previous post whose code can be found on GitHub.

While the previous post focused on the ToDoService component, this post will concentrate on setting up the ASP.NET Core pipeline.

Here is the Program.cs file of the web application.

var builder = WebApplication.CreateBuilder(args);

// Configures Entity Framework Core to target a PostgreSQL database
builder.AddNpgsqlDbContext<TodoDbContext>("pgdb");

builder.Services.AddTransient<ToDoService>();

var app = builder.Build();

// Defines an endpoint group
var todos = app.MapGroup("todos");

// Defines an endpoint accepting GET requests without parameters
todos.MapGet("/", (ToDoService svc) => svc.GetAll());

// Defines an endpoint accepting POST requests
todos.MapPost("/", async (ToDoService svc, TodoItem item) => {
    item.Id = Guid.NewGuid();
    item.CreatedAt = DateTime.UtcNow;

    await svc.Add(item);

    return Results.StatusCode(StatusCodes.Status201Created);
});

// Forces Entity Framework Core to ensure that database tables are created
if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();

    using var db = scope.ServiceProvider.GetRequiredService<TodoDbContext>();

    await db.Database.EnsureCreatedAsync();
}

app.Run();

In the snippet above,

  • we configure Entity Framework Core to target a PostgreSQL database named pgdb
  • we declare an endpoint group and attach two endpoints to it
  • we make sure that the database tables are created.

With the current setup, the application will accept:

  • GET requests to /todos and return all the items returned by the service,
  • POST requests to /todos to let the service register a new item.

The repository includes a API.http file that includes samples of valid requests to the server.

Similarly to the previous post, we want to create a NUnit test project that allows us to fire HTTP requests to these two endpoints and make some assertions on the related response.

The following sketch outlines our testing goals, though it includes a placeholder method (GetHttpClient()) that we will define later.

[Test]
public async Task GET_should_return_all_items()
{
    var http = GetHttpClient(); // 👈 COMPILER ERROR

    var items = await http.GetFromJsonAsync<TodoItem[]>("/todos", default);

    Assert.That(items?.Length ?? 0, Is.EqualTo(Database.Items.Count()));
}

[Test]
public async Task POST_should_create_new_item()
{
    var item = new TodoItem
    {
        Id = Guid.NewGuid(),
        Title = "Test",
        Description = "Some very important description",
        CreatedAt = DateTime.UtcNow,
        IsComplete = false
    };

    var http = GetHttpClient(); // 👈 COMPILER ERROR

    using var response = await http.PostAsJsonAsync("/todos", item, default);

    Assert.That(response.IsSuccessStatusCode, Is.True);
}

WebApplicationFactory and top-level statements

At the time of writing, ASP.NET Core didn’t have support for Minimal API endpoints and, most importantly, C# didn’t support top-level statements.

Things have changed since then and some workarounds need to be implemented to make WebApplicationFactory work correctly with newer ASP.NET Core applications.

The first change is that newer ASP.NET Core applications don’t have a Startup class anymore. Originally, we would reference the Startup class when building a new WebApplicationFactory. Since there is no Startup class anymore, the WebApplicationFactory supports targeting the Program class that is now responsible for configuring the ASP.NET Core pipeline and all the needed services.

// OLD
var factory = new WebApplicationFactory<Startup>();

// NEW
var factory = new WebApplicationFactory<Program>();

Unfortunately, simply targeting the Program class won’t work. The reason is that, when using top-level statements, the compiler creates the Program class and wraps the code of the Program.cs file into a Main method. By default, the class is not visible outside of the assembly, so the test project can’t reference it.

Luckily, the generated Program class is also marked as partial. We can leverage that to change its visibility. This means that to change the visibility of the Program class generated by the compiler, we simply need to add the line below in a new file.

public partial class Program {}

This will make the class visible outside of the assembly and allow our test project to reference it when instantiating the WebApplicationFactory<Program>.

Including the WebApplicationFactory

In the previous post, we leveraged NUnit fixture lifecycle attributes to encapsulate code that needs to be executed before tests are executed.

We used a method tagged with the [OneTimeSetUp] attribute to start the database container once and one tagged with the [OneTimeTearDown] to stop it.

These methods are also the natural point for our WebApplicationFactory setup and disposal.

[SetUpFixture]
public class Tests
{
    private static readonly PostgreSqlContainer DatabaseContainer = new PostgreSqlBuilder()
      .WithImage("postgres:latest")
      .WithDatabase("mydatabase")
      .WithUsername("myusername")
      .WithPassword("mypassword")
      .WithWaitStrategy(Wait
        .ForUnixContainer()
        .UntilMessageIsLogged("database system is ready to accept connections"))
      .Build();

    private static Respawner respawner = default!;

    private static RespawnerOptions respawnerOptions = new() { DbAdapter = DbAdapter.Postgres };

    // Exposes the WebApplicationFactory for other classes to access it
    public static WebApplicationFactory<Program> WebApplicationFactory = default!;

    public static string ConnectionString => DatabaseContainer.GetConnectionString();

    [OneTimeSetUp]
    public async Task OneTimeSetUp()
    {
        await DatabaseContainer.StartAsync();

        // Initializes the WebApplication and feeds it the connection string
        WebApplicationFactory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(web => web.UseSetting("ConnectionStrings:pgdb", ConnectionString));

        // Creates a discarded client to actually start the web application
        _ = WebApplicationFactory.CreateDefaultClient();

        await using var connection = new NpgsqlConnection(ConnectionString);

        await connection.OpenAsync();

        respawner = await Respawner.CreateAsync(connection, respawnerOptions);
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown()
    {
        // Disposes the WebApplicationFactory
        await WebApplicationFactory.DisposeAsync();

        await DatabaseContainer.StopAsync();
        await DatabaseContainer.DisposeAsync();
    }

    public static async Task ResetDatabase()
    {
        await using var connection = new NpgsqlConnection(ConnectionString);

        await connection.OpenAsync();

        await respawner.ResetAsync(connection);
    }
}

The class above is very similar to the one used for the unit tests with some notable differences:

  • A static field holding the WebApplicationFactory instance is added and made accessible to other classes,
  • The OneTimeSetUp() method is not responsible for ensuring that the database is correctly created,
  • The WebApplicationFactory is initialized and configured in the OneTimeSetUp() method,
  • The WebApplicationFactory is disposed in the OneTimeTearDown() method.

Targeting the web application in the tests

Now that we got an instance of our web application running thanks to the WebApplicationFactory, we can rewrite our tests to target it.

To do so, we can replicate the approach used in the previous post and create an abstract test fixture class that provides its inheritors some utilities.

[TestFixture]
public abstract class TestsWithApp
{
  protected TodoDbContext Database = default!;
  protected HttpClient WebApp = default!;

  [SetUp]
  public void SetUp()
  {
    var options = new DbContextOptionsBuilder<TodoDbContext>()
      .UseNpgsql(Tests.ConnectionString)
      .Options;

    Database = new TodoDbContext(options);

    WebApp = Tests.WebApplicationFactory.CreateClient();
  }

  [TearDown]
  public async Task TearDown()
  {
    await Tests.ResetDatabase();

    WebApp.Dispose();
    Database.Dispose();
  }
}

The abstract class detailed above offers to its inheritors two utilities:

  • An instance of the HttpClient customized so that it targets the instance of the web application,
  • An instance of the ToDoDbContext that allows the test to alter the database in the arrange phase or check its status in the assert phase.

As usual, the TearDown() method is used to dispose all the resources and reset the database using Respawn.

With the TestsWithApp class in place, we can write our test fixture as follows:

[TestFixture]
public class ToDoTests : TestsWithApp
{
    [Test]
    public async Task GET_should_return_all_items()
    {
        var items = await WebApp.GetFromJsonAsync<TodoItem[]>("/todos", default);

        Assert.That(items?.Length ?? 0, Is.EqualTo(Database.Items.Count()));
    }

    [Test]
    public async Task POST_should_create_new_item()
    {
        var item = new TodoItem
        {
            Id = Guid.NewGuid(),
            Title = "Test",
            Description = "Some very important description",
            CreatedAt = DateTime.UtcNow,
            IsComplete = false
        };

        using var response = await WebApp.PostAsJsonAsync("/todos", item, default);

        Assert.That(response.IsSuccessStatusCode, Is.True);
    }
}

In the test fixture above, the tests are using the WebApp property to fire HTTP requests at the web application.

You can find the final version of the code snippets in this GitHub repository: https://github.com/Kralizek/EntityFrameworkCoreTesting

Recap

In this post we have used the libraries and approaches learned in the previous post to write end-to-end tests that target our ASP.NET Core application using Entity Framework to access and modify its database.

Support this blog

If you liked this article, consider supporting this blog by buying me a pizza!