Minimal APIs have been around for quite some times now and have considerably reduced the amount of boilerplate code needed for some projects.
However, good practices are still to be enforced and even tho the approach on creating API might have evolved, integration testing has not disappeared.
In this post, we will see, step by step, how to bring integration testing to your ASP.NET (7) minimal API in different scenarios.
Overview
- Our minimal API
- Setup and tooling
- Testing a simple call
- Testing the content of the response
- Testing using custom services
- Using fixtures
Our minimal API
You can find the source code of this article on GitHub here
Let's start by creating our app:
# Create our solution
dotnet new sln
# Create our minimal API and add it to our solution
dotnet new webapi -minimal --no-openapi -o Api
dotnet sln add Api
Open your solution in the editor of your choice and replace the content of the Program.cs
by the following, to get rid of the boilerplate:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run();
Creating our endpoint
Our minimal API will be dealing with TODO items.
First, let's add our model:
public record TodoItem(Guid Id, string Title, bool IsDone);
Our service won't have much logic nor code:
public interface ITodoItemService
{
TodoItem AddTodoItem(string title);
}
internal class TodoItemService : ITodoItemService
{
private readonly List<TodoItem> _todoItems = new();
public TodoItem AddTodoItem(string title)
{
var todoItem = new TodoItem(Guid.NewGuid(), title, false);
_todoItems.Add(todoItem);
return todoItem;
}
}
With our endpoint and the service registration, our Program.cs
now looks like this:
var builder = WebApplication.CreateBuilder(args);
+ builder.Services.AddTransient<ITodoItemService, TodoItemService>();
var app = builder.Build();
+ app.MapPost(
+ "/api/todo-items",
+ (TodoItemCreationDto dto, ITodoItemService todoItemService)
+ => todoItemService.AddTodoItem(dto.Title));
app.Run();
+ public record TodoItemCreationDto(string Title);
+ public record TodoItem(Guid Id, string Title, bool IsDone);
+ public interface ITodoItemService { /* ... */ }
+ internal class TodoItemService : ITodoItemService { /* ... */ }
We're all set!
Setup and tooling
Now that our API has an endpoint that can be called, we can add our integration test project.
For our example, I will use xUnit but feel free to use any testing framework you are familiar with:
dotnet new xunit -n Api.IntegrationTests
dotnet sln add Api.IntegrationTests
dotnet add Api.IntegrationTests reference Api
We will also need to access the ASP.NET Core framework from this project. We can do so by adding the Microsoft.AspNetCore.Mvc.Testing
NuGet to our project:
dotnet add Api.IntegrationTests package Microsoft.AspNetCore.Mvc.Testing
The first change we need to make is in the Api.IntegrationTests.csproj
where we need to specify that we will be using the Web SDK for this test project:
- <Project Sdk="Microsoft.NET.Sdk">
+ <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>...</PropertyGroup>
<ItemGroup>...</ItemGroup>
</Project>
We're good to go!
Testing a simple call
Now that our test project is created, we would like to:
- Initialize the application
- Create an
HttpClient
to interact with it - Make a call to our endpoint to create a
TodoItem
- Ensure that the response is our
TodoItem
Let's break this down.
Creating our test
First, let's create our test in a new test file:
namespace Api.IntegrationTests;
public class TodoItemCreationEndpointTest
{
[Fact]
public async Task CreatingATodoItemShouldReturnIt() { }
}
Creating the application can be done using the WebApplicationFactory
to further create a client from it:
// Arrange
var payload = new TodoItemCreationDto("My todo");
await using var application = new WebApplicationFactory<Program>();
using var client = application.CreateClient();
Using this client, we can send the payload on our endpoints route:
// Act
var result = await client.PostAsJsonAsync("/api/todo-items", payload);
And finally, from this response we can test its status code:
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Our test should now look like this:
[Fact]
public async Task CreatingATodoItemShouldReturnIt()
{
// Arrange
var payload = new TodoItemCreationDto("My todo");
await using var application = new WebApplicationFactory<Program>();
using var client = application.CreateClient();
// Act
var result = await client.PostAsJsonAsync("/api/todo-items", payload);
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
Let's run this and ... everything blows up!
Tweaking our API to access it
By default, when compiling our Program.cs
file, our Program
class will have the the private
modifier and therefore not being accessible from our testing assembly.
In fact, the Program
class we were using in our WebApplicationFactory
was not ours as the intellisense shows us:
To fix that, we simply need to add one line at the end of our Program.cs
file to indicate ASP.NET to generate this as a public class:
// ...
public partial class Program { }
A quick look at the intellisense will confirm that our modification has worked and that we are now using our own Program
:
And the test now passes:
Testing the content of the response
Having the response's status code is a good thing but testing the actual content would be better.
To do so, we can simply deserialize the response:
[Fact]
public async Task CreatingATodoItemShouldReturnIt()
{
// Arrange
var payload = new TodoItemCreationDto("My todo");
await using var application = new WebApplicationFactory<Program>();
using var client = application.CreateClient();
// Act
var result = await client.PostAsJsonAsync("/api/todo-items", payload);
+ var content = await result.Content.ReadFromJsonAsync<TodoItem>();
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
+ Assert.Equal("My todo", content?.Title);
+ Assert.False(content?.IsDone);
}
Testing using custom services
Our testing journey is going great but there is a small issue here.
For new, we are using the TodoItemService
through the provided ITodoItemService
and this service might rely on another service that you might not want to call (such as an email provider, a SaaS where you must pay at each call, etc.).
It would be great if we could customize the dependency injection container so that it will be using our own dependency and not the application's one.
We can do so by changing the service configuration using the WithWebHostBuilder
extension method:
// 👇 Test Double for our `ITodoItemService`
internal class TestTodoItemService : ITodoItemService
{
public TodoItem AddTodoItem(string title)
=> new(Guid.Empty, "test", false);
}
public class TodoItemCreationEndpointTest
{
[Fact]
public async Task CreatingATodoItemShouldReturnIt()
{
// Arrange
var payload = new TodoItemCreationDto("My todo");
await using var application = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
{
// 👇 Registration of our Test Double
services.AddScoped<ITodoItemService, TestTodoItemService>();
}));
using var client = application.CreateClient();
// Act
var result = await client.PostAsJsonAsync("/api/todo-items", payload);
var content = await result.Content.ReadFromJsonAsync<TodoItem>();
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// 👇 We should now be using our Test Double response
Assert.Equal("test", content?.Title);
Assert.False(content?.IsDone);
}
}
Using fixtures
For now we only have a simple test but we might want to have a more complex scenario, that is split across multiple tests.
In order to keep the same context from test to test, we can take advantage of xUnit fixtures.
Overview
There is two kind of fixtures:
The class fixture which is to be used "when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished."
The collection fixture which is to be used "when you want to create a single test context and share it among tests in several test classes, and have it cleaned up after all the tests in the test classes have finished."
I will leave up to you the decision of using one or the other and for our irrelevant example we will use the class fixture.
Implementing the IClassFixture
The interface IClassFixture
accepts up to one type parameter, which is the type of the fixture. A fixture having a type parameter must also have a constructor with one parameter of the same type.
Here is how we could write a class fixture sharing our WebApplicationFactory<Program>
:
public abstract class IntegrationTestBase : IClassFixture<WebApplicationFactory<Program>>
{
// 👇 Parameter matching the type of the `IClassFixture`
public IntegrationTestBase(WebApplicationFactory<Program> factory) { }
}
Let's take advantage of this behavior to share the same HttpClient
across our tests:
public abstract class IntegrationTestBase : IClassFixture<WebApplicationFactory<Program>>
{
protected readonly HttpClient Client;
public IntegrationTestBase(WebApplicationFactory<Program> factory)
=> Client = factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
{
services.AddScoped<ITodoItemService, TestTodoItemService>();
}))
.CreateClient();
}
And finally, let's refactor our former test by using our fixture:
- public class TodoItemCreationEndpointTest
+ public class TodoItemCreationEndpointTest : IntegrationTestBase
{
+ public TodoItemCreationEndpointTest(WebApplicationFactory<Program> factory)
+ : base(factory) { }
[Fact]
public async Task CreatingATodoItemShouldReturnIt()
{
// Arrange
var payload = new TodoItemCreationDto("My todo");
- await using var application = new WebApplicationFactory<Program>()
- .WithWebHostBuilder(builder => builder.ConfigureServices(services =>
- {
- services.AddScoped<ITodoItemService, TestTodoItemService>();
- }));
- using var client = application.CreateClient();
// Act
- var result = await client.PostAsJsonAsync("/api/todo-items", payload);
+ var result = await Client.PostAsJsonAsync("/api/todo-items", payload);
var content = await result.Content.ReadFromJsonAsync<TodoItem>();
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal("test", content?.Title);
Assert.False(content?.IsDone);
}
}
And that's it!
Final words
In this post, we saw how to get started with integration testing in you ASP.NET Minimal API by creating a very simple ASP.NET Minimal API and successively:
- Creating a test project for it, adapted to the web nature of our API
- Querying it from an
HttpClient
and making assertions on the result - Lifting the creation of the
HttpClient
in aIClassFixture
in order for all tests to share the same client
In real life applications, you might need some more advanced concepts (handling authentication, etc.) and, in that case, I highly encourage you checking other resources such as the official .NET documentation on this topic or this article by Twilio.
If you are working with an Entity Framework DbContext
, you might also want to have a look at their documentation on how to use a test one for your fixture.
In any case, I hope that this has been a good introduction that can help you to strengthen your testing strategy and will be a nice addition to your unit tests suites.
As always, happy coding!
Photo by Hans Reniers on Unsplash
Top comments (0)