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
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.
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
}
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 🚀
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