Getting started with integration testing for your Minimal API
Tags: csharp, dotnet, testing
Published: 2023-01-19 16:19:06

[Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview?view=aspnetcore-7.0) 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](https://martinfowler.com/bliki/IntegrationTest.html) 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

1. Our minimal API
1. Setup and tooling
1. Testing a simple call
1. Testing the content of the response
1. Testing using custom services
1. Using fixtures

---

### Our minimal API

> You can find the source code of this article on GitHub [here](https://github.com/pBouillon/DEV.GettingStartedWithIntegrationTesting)

Let's start by creating our app:

```sh
# 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:

```csharp
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:

```csharp
public record TodoItem(Guid Id, string Title, bool IsDone);
```

Our service won't have much logic nor code:

```csharp
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:

```diff
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:

```sh
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:

```sh
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:

```diff
- <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:

```csharp
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:

```csharp
// 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:

```csharp
// Act
var result = await client.PostAsJsonAsync("/api/todo-items", payload);
```

And finally, from this response we can test its status code:

```csharp
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
```

Our test should now look like this:

```csharp
[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!

![Failing test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ti6lz413n5bw37i8yvxw.png)

#### 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:

![Program class intellisense](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvbdz4ub527zyvaqren2.png)

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:

```csharp
// ...
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`:

![Fixed Program class intellisense](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6a0y52o450mhzmkhtj4.png)

And the test now passes:

![Passing test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nbk30vqsamo1h9mo8xe0.png)

### 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:

```diff
[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:

```csharp
// 👇 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](https://xunit.net/docs/shared-context#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](https://xunit.net/docs/shared-context#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>`:

```csharp
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:

```csharp
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:

```diff
- 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 a `IClassFixture` 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](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/test-min-api?view=aspnetcore-7.0) or [this article by Twilio](https://www.twilio.com/blog/test-aspnetcore-minimal-apis).

If you are working with an Entity Framework `DbContext`, you might also want to have a look at [their documentation]([](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-7.0#customize-webapplicationfactory)) 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!

---

{% embed https://dev.to/pbouillon %}

---

Photo by <a href="https://unsplash.com/es/@hansreniers?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Hans Reniers</a> on <a href="https://unsplash.com/photos/lQGJCMY5qcM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>