Integration Testing and Minimal API in .Net 7

Say Hello to Reliable Minimal APIs with Integration Tests

Integration Testing and Minimal API in .Net 7

Hi there! Integration testing is an important part of the software development process because it helps ensure that your APIs are working correctly and returning the expected result. Unfortunately, many people often mix up integration tests with mock testing and use mock for integration tests. This can lead to issues when testing APIs, as mock tests don’t provide a complete picture of how your code will behave in a real-world scenario.

In this blog post, I’ll show you how to create a reliable todo API service using Minimal APIs in .Net 7 and write the needed integration tests using WebApplicationFactory and XUnit. We’ll be covering four different operations: getting a single todo item, getting all todo items, posting a new todo item, and updating an existing todo item.

You can find the full source code for this tutorial on GitHub at mhdbouk/minimal-api-integration-tests

Stay in touch

Subscribe to our mailing list to stay updated on topics and videos related to .NET, Azure, and DevOps!

Creating the Minimal API Project and XUnit Project

First things first, let’s create a new minimal API project in .NET 7 and an XUnit project for our integration tests.

// Create new directory for the solution
mkdir MinimalApiDemo
cd MinimalApiDemo

// Add new webapi using minimal apis instead of controllers
dotnet new webapi -minimal -o MinimalApiDemo.Api

// Add new xunit test project
dotnet new xunit -o MinimalApiDemo.Tests

// Add Api project refenrece in the test project
dotnet add MinimalApiDemo.Tests reference MinimalApiDemo.Api

// Create new Solution and add the 2 projects
dotnet new sln
dotnet sln add MinimalApiDemo.Api
dotnet sln add MinimalApiDemo.Tests

Open the solution we just created in Visual Studio and start by deleting the contents of Program.cs in MinimalApiDemo.Api and replace it with the following

// Program.cs

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// APIs goes here

app.Run();

Defining the Minimal API Endpoints

To create a minimal API endpoint, you can use the MapGet, MapPost, and MapPut methods of the WebApplication object (called app in our example) to define your endpoints. We also going to have a TodoService that will handle all the todo logic.

TodoService & EF Context

I’m going to reuse the TodoService class from my previous blog post Build your own cli because you can – mdbouk.com with a slight change to support the use of an EF context. This will allow us to configure our tests to use a test database connection string instead of the production connection string.

First we need to register the TodoService in our Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<ITodoService, TodoService>();

Then we should configure our TodoDbContext to use Sqlite

// Program.cs

builder.Services.AddDbContext<TodoDbContext>(options =>
{
    var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
    options.UseSqlite($"Data Source={Path.Join(path, "MinimalApiDemo.db")}");
});

And now we can create our minimal APIs

// Program.cs

app.MapGet("/todo/{id}", async (int id, ITodoService todoService) =>
{
    var item = await todoService.GetItemAsync(id);
    if (item == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(item);
});

app.MapGet("/todo/", (ITodoService todoService) => todoService.GetItemsAsync());

app.MapPost("/todo/", async (TodoItem item, ITodoService todoService) =>
{
    if (item is null)
    {
        return Results.BadRequest("Body is null");
    }
    if (string.IsNullOrWhiteSpace(item.Title))
    {
        return Results.BadRequest("Title is null");
    }
    var result = await todoService.AddItemAsync(item.Title!);
    return Results.Created($"/todo/{result.Id}", result);
});

app.MapPut("/todo/{id}", async (int id, TodoItem item, ITodoService todoService) =>
{
    var existingItem = await todoService.GetItemAsync(id);
    if (existingItem == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(todoService.UpdateItemAsync(id, item.Title!));
});

app.MapPut("/todo/{id}/done", async (int id, ITodoService todoService) =>
{
    var existingItem = await todoService.GetItemAsync(id);
    if (existingItem == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(await todoService.MarkAsDoneAsync(id));
});

Creating an integration test class with WebApplicationFactory

Now it’s time to write our integration tests. The goal here is to test the API without mocking any of the dependencies. This means that we will be making actual HTTP calls to the API and testing the response. Let’s get started!

To do that we need to use the WebApplicationFactory class to create the web host of our application. In MinimalApiDemo.Tests project, create a new class called TodoTests.cs and add the following

public class TodoTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _httpClient;

    public TodoTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _httpClient = _factory.CreateClient();
    }
    
    // Add Tests here
}

IClassFixture

In xUnit, you can use the interface IClassFixture<TFixture> to create and clean up a class fixture. To access the fixture data inside the test, you can add a constructor argument to your test class that matches the type parameter TFixture. For example, in our case, we can use WebApplicationFactory<Program>.

Now we can add our test method

[Fact]
public async Task AddTodoItem_ReturnsCreatedSuccess()
{
    // Arrange
    var todoItem = new TodoItem { Title = "Cool Integration Test Item" };
    var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(todoItem), Encoding.UTF8, "application/json");

    // Act
    var response = await _httpClient.PostAsync("/todo/", content);
    var responseContent = await response.Content.ReadAsStringAsync();
    var item = System.Text.Json.JsonSerializer.Deserialize<TodoItem>(responseContent, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));

    // Assert
    Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    Assert.NotNull(item);
    Assert.NotNull(response.Headers.Location);
    Assert.Equal(response.Headers.Location.ToString(), $"/todo/{item.Id}");
}

In this test, we first arrange our data by preparing the body content of our POST request. Next, we make the actual HTTP call by calling the PostAsync method on our _httpClient instance, which was created by the WebApplicationFactory. Finally, we assert that the response was successful by checking the status code and verifying that certain properties of the response are not null. Specifically, we check that the status code is HttpStatusCode.Created, that the response content (deserialized as a TodoItem object) is not null, that the Location header is present, and that its value matches the expected URI for the newly-created todo item.

Configuring the Test Database Connection String

Great! Now that we’ve got our integration test set up and running, let’s talk about how we can modify it to use a test database instead of the production one. This is important because we don’t want to accidentally modify or corrupt our production data while running our tests.

To accomplish this, we can create a new class called TodoWebApplicationFactory that inherits from WebApplicationFactory<TProgram>.

We can then override the ConfigureWebHost method to specify our test database connection string and set the environment to “Development” or “Testing”. This ensures that any changes made during our integration tests will not affect the production database.

With this setup, we can now write our integration tests with confidence, knowing that we are not impacting the production environment.

public class TodoWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TodoDbContext>));

            services.Remove(dbContextDescriptor!);

            services.AddDbContext<TodoDbContext>(options =>
            {
                var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
                options.UseSqlite($"Data Source={Path.Join(path, "MinimalApiDemoTests.db")}");
            });
        });
        
        builder.UseEnvironment("development");
    }
}

And now we need to change our TodoTests class to implement IClassFixture> from the new TodoWebApplicationFactory<TProgram> and passing Program as the type parameter

public class TodoTests : IClassFixture<TodoWebApplicationFactory<Program>>
{
    private readonly TodoWebApplicationFactory<Program> _factory;
    private readonly HttpClient _httpClient;

    public TodoTests(TodoWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _httpClient = _factory.CreateClient();
    }

    [Fact]
    public async Task AddTodoItem_ReturnsCreatedSuccess()
    {
      ...
    }
}

Now, when we execute the tests, it will use the new connection string and the new environment (if our Program is using any environment-specific configurations). This allows us to test our API using a separate database and environment, ensuring that our tests are not affected by any changes made to the production configuration. This is especially important when running automated tests as part of a continuous integration and deployment pipeline, as it ensures that our tests are consistent and reliable.

Configuring Other Services in Tests

While testing our minimal API, there may be cases where we need to configure other services that our API relies on, such as an email provider. In this section, we will discuss how to set up these services in such a way that they do not send emails when running tests.

One way to do this is to use dependency injection to pass in a mock or fake implementation of the service. This way, we can control the behavior of the service and ensure that it does not send any emails during testing.

// Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<ITodoService, TodoService>();
builder.Services.AddSingleton<IEmailProvider, SendGridEmailProvider>();

Create a new FakeEmailProvider in MinimalApiDemo.Tests with the following implementation

public class FakeEmailProvider : IEmailProvider
{
    public Task SendAsync(string emailAddress, string body)
    {
        // We don't want to actually send real emails when running integration tests.
        return Task.CompletedTask;
    }
}

And now we can configure our TodoWebApplicationFactory to register the fake email provider as below

Spublic class TodoWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Fake Email Provider
            var emailDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IEmailProvider));

            services.Remove(emailDescriptor!);

            services.AddSingleton<IEmailProvider, FakeEmailProvider>();
            
            ...
        });
    }
}

Summary

This tutorial taught us how to write integration tests for minimal APIs using .NET 7, WebApplicationFactory, and XUnit. We started by creating a minimal API project and an XUnit project for our integration tests. Then, we used the WebApplicationFactory class to create the web host of our application and the TodoTests class to implement IClassFixture<WebApplicationFactory>. This allowed us to test the API by making actual HTTP calls and testing the response.

We also learned how to override the default connection string and environment variables in the TodoWebApplicationFactory inherited from WebApplicationFactory. This allowed us to use a test database connection string and set the environment to “development” or “testing” during our tests.

Additionally, we learned how to configure other services, such as email providers, to not send real emails when running the tests.

Overall, this tutorial demonstrated the importance of integration testing and how to effectively set it up to test our APIs.

If you have any questions or feedback, please don’t hesitate to leave a comment below. Happy coding 🚀

Recent Posts

Azure-Sync: Sync your Azure App Settings to local
Azure-Sync is a handy shell script tool designed to help .NET developers working with Azure App Services. Inspired by the functionality provided by the Azure …
Implement Builders easily with Source Generator in .NET
I created a YouTube video on Source Generator in which I showcased one possible implementation. However, I feel that I didn’t fully highlight its capabilities. …
Secure On-Premise .NET Application with Azure Key Vault
Suppose you have your Web App and Database server hosted locally on your On-Premises servers. You want to use Azure Key Vault with your .NET …
Running Integration Tests with Docker in .NET using TestContainers
Hello everyone, in today's post I will show you the easiest and cleanest way to perform integration testing of your code with database dependencies with …

1 thought on “Say Hello to Reliable Minimal APIs with Integration Tests

  1. Andrzej

    Unfortunately I can see you copied the bad patterns from MSDN sample, as many people do.

    I.e. doing this:
    `public class TodoTests : IClassFixture<TodoWebApplicationFactory>`

    First of all, TodoWebApplicationFactory should not take generic parameter, since it’s and will be constant.
    It should be simply:
    class TodoWebApplicationFactory : WebApplicationFactory

    Second problem, the Fixture should be a separate class that represents the whole SUT, not only the service. E.g. class TodoFixture {}
    that has components like:
    – TodoWebApplicationFactory
    – Database
    – ExternalService mock
    – etc.

    This way you have a clean separation of concerns and easier maintenance in the future

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.