Testing ASP.NET Core endpoints that use Entity Framework Core
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 theOneTimeSetUp()
method, - The
WebApplicationFactory
is disposed in theOneTimeTearDown()
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!