Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 19, 2023 04:19 pm GMT

Getting started with integration testing for your Minimal API

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

  1. Our minimal API
  2. Setup and tooling
  3. Testing a simple call
  4. Testing the content of the response
  5. Testing using custom services
  6. 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 solutiondotnet new sln# Create our minimal API and add it to our solutiondotnet new webapi -minimal --no-openapi -o Apidotnet 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.IntegrationTestsdotnet sln add Api.IntegrationTestsdotnet 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:

// Arrangevar 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:

// Actvar result = await client.PostAsJsonAsync("/api/todo-items", payload);

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

// AssertAssert.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!

Failing test

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

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:

Fixed Program class intellisense

And the test now passes:

Passing test

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 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 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!

Picture by Josh Redd on Unsplash


Original Link: https://dev.to/this-is-learning/getting-started-with-integration-testing-for-your-minimal-api-3j0l

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To