Testing an API with TestServer

I am writing this post mainly because I receive questions almost every week about how we can test our APIs, and if it's possible to do it without docker, in other words, in-memory.

 

The answer is yes, and now we are going to see how to do it. 

 

 

For this post, note that most of the code is written in NetCore 3.1, which is important to mention since it changes in .NET6; still, I will include the code snippet so you can see the difference (it's minimal). 

 

 

1 - How to create API Tests in .NET? 

To test our APIs, we will use the TestServer type provided to us by Microsoft. 

 

Of course, we need to have an API available in our application; we can choose any endpoint. For the first example, I will use an endpoint that has no dependencies:

[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
public class AcademicProjectsController : ControllerBase
{
    [HttpGet("{userId}")]
    public Task<AcademicProjectsDto> Get(int userId)
    {
        if (userId != 1)
            throw new NotImplementedException();

        //TODO: Demo - this is to simulate a real scenario
        var academicProjets = new AcademicProjectsDto()
        {
            Projects = new List<AcademicProjectDto>()
            {
                new AcademicProjectDto()
                {
                    Id=1,
                    Details = "Aplicación para suibr imagenes a internet, con la posiblidad de retocarlas con filtros y redimensionar",
                    Environment = new List<string>(){"PHP","JavaScript", "Bootstrap"},
                    Name = "IMGLovely"
                }
            }
        };
        return Task.FromResult(academicProjets);
    }
}

 

Now, we need to create a project (we can use an existing one) to perform our tests. Personally, I like to separate the tests. Some people call these integration tests, but I have already explained what integration tests mean to me integration tests. So, in my opinion, we can call them API tests or Component tests.

And in the project, we must add two references: one to the project we want to test (your API) and another to the NuGet library Microsoft.AspNetCore.TestHost.

 

And we create a test where we instantiate WebHostBuilder, which will allow us to build a web host and later the TestServer type that will build the web host in-memory:

[Fact]
public async Task WhenCallAPI_withID1_thenResult()
{
    var webHostBuilder =
        new WebHostBuilder()
            .UseStartup<Startup>(); 

    using (var server = new TestServer(webHostBuilder))
    {
       //Código
    }
}

 

Now, we only need to call the code. For this, TestsServer provides us with a method called .CreateClient, which gives us an HTTP client that interacts with the in-memory server it just set up for us. 

This functionality is awesome because we can include headers or different information we may need, such as authentication, to that HTTP Client

 

In our particular case, we do not need to add additional information, so we make a call to the endpoint:

 

[Fact]
public async Task WhenCallAPI_withID1_thenResult()
{
    IWebHostBuilder webHostBuilder =
        new WebHostBuilder()
            .UseStartup<Startup>(); 

    using (TestServer server = new TestServer(webHostBuilder))
    using (HttpClient client = server.CreateClient())
    {
        AcademicProjectsDto result = await client.GetFromJsonAsync<AcademicProjectsDto>("/api/AcademicProjects/1", 
            new JsonSerializerOptions(){PropertyNameCaseInsensitive = true});
    
        Assert.Equal(1, result.Projects.First().Id);
    }
}

And with that, we have our test in the API.

 

 

2 - How does TestServer work

I want to take a moment to explain that when we use TestServer, we are passing all the configuration through WebHostBuilder, which requires us to use the extension method .UseStartup<T> where T is our Startup class in the API to test. This is because it will use reflection to read the ConfigureServices method and load all the information from it.

 

2.1 - Add configuration to TestServer

It's very common that in ConfigureServices we access the configuration information inside our app's configuration (the appsettings.json); 

 

But at the same time, we're running tests, so it's very likely that we want to change part of that configuration. For that, the library provides us with an extension method called .ConfigureAppConfiguration(), which receives a delegate to indicate this configuration.

 

One option is to add a file that we have inside the test project: 

IWebHostBuilder webHostBuilder =
    new WebHostBuilder()
        .ConfigureAppConfiguration(x => x.AddJsonFile("appsettings.tests.json", optional: true))
        .UseStartup<Startup>();

 

But we can also create that IConfiguration in-memory inside the test and pass it into .ConfigureAppConfiguration.

 

2.2 - Change the environment in TestServer

It's possible that depending on the environment we're in, our code should behave differently. For that, we have the .UseEnvironment() method, which allows us to specify an environment that simulates the ASPNETCORE_ENVIRONMENT environment variable.

IWebHostBuilder webHostBuilder =
    new WebHostBuilder()
        .UseEnvironment("production")
        .UseStartup<Startup>();

In our case, it's as if we're running the code in production.

 

 

3 - Working with dependencies in TestServer

A very important point is, what happens with dependencies? We are running tests, and it's possible that some of our APIs make insertions into the database, send messages to a service bus, or call other APIs.

 

In previous videos, we saw how to use mock to deal with our dependencies. And now we must do something similar.

 

In some cases, we will mock the element we don't want to execute in the code (for example, a database), but it's also very common to use a stub or a fake.

The difference between them is that a stub always provides the same responses, and a fake stores information, but has limited functionalities and only for the test. 

For example, in a database, if you make a fake, you'll store the elements in a list, and then in your test, you can compare those elements.

Meanwhile, a stub always returns the same information whether you insert or query.

 

 

 To configure this information, WebHostBuilder provides us with the .ConfigureTestServices() method

 

  • Note: we also have .ConfigureServices(); just keep in mind that .ConfigureTestServices is executed afterwards. This is important because if you are inserting/modifying information and put your mock/fake/stub in ConfigureServices you run the risk of affecting the real database. 

 

For this example, we are going to use the endpoint with a post:

namespace WebPersonal.BackEnd.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    [Produces("application/json")]
    public class PerfilPersonalController : ControllerBase
    {
        [HttpPost("returnonlyid")]
        public async Task<Result<int?>> PostId(PersonalProfileDto profileDto)
        {
            return await _postPersonalProfile.Create(profileDto)
                .MapAsync(x=>x.UserId);
        }
    }
}

 

Which creates a profile inside the system, and we are going to create a stub for the dependencies:

public class StubIPostPersonalProfileDependencies : IPostPersonalProfileDependencies
{
    public Task<UserIdEntity> InsertUserId(string name)
        => Task.FromResult(UserIdEntity.Create(name, 1));

    public Task<Result<PersonalProfileEntity>> InsertPersonalProfile(PersonalProfileEntity personalProfile)
        => PersonalProfileEntity.Create(personalProfile.UserId, personalProfile.Id, personalProfile.FirstName,
            personalProfile.LastName, personalProfile.Description, personalProfile.Phone,
            personalProfile.Email, personalProfile.Website, personalProfile.GitHub).Success().Async();

    public Task<Result<List<SkillEntity>>> InsertSkills(List<SkillEntity> skills)
        => skills.Select(a => SkillEntity.Create(a.UserId, a.Id, a.Name, a.Punctuation)).ToList().Success().Async();

    public Task<Result<List<InterestEntity>>> InsertInterests(List<InterestEntity> interests)
        => interests.Select(a => InterestEntity.Create(a.Id, a.UserId, a.Description)).ToList().Success().Async();

    public Task<Result<bool>> SendEmail(string to, string subject, string body)
        => true.Success().Async();

    public Task CommitTransaction()
        => Task.CompletedTask;
}

 

The important part is that when we use .ConfigureTestServices, what we are doing is replacing the dependencies that our API has with those specified here, so we must include the following code:

IWebHostBuilder webHostBuilder =
    new WebHostBuilder()
        .ConfigureTestServices(serviceCollection =>
        {
            serviceCollection
                .AddScoped<IPostPersonalProfileDependencies, StubIPostPersonalProfileDependencies>();
        })
        .UseStartup<Startup>();

 

Finally, we make the API call with the post and the serialized content. 

[Fact]
public async Task WhenInsertInformation_returnCorrect()
{
    IWebHostBuilder webHostBuilder =
        new WebHostBuilder()
            .ConfigureTestServices(serviceCollection =>
            {
                serviceCollection
                    .AddScoped<IPostPersonalProfileDependencies, StubIPostPersonalProfileDependencies>();
            })
            .UseStartup<Startup>();
    
    PersonalProfileDto defaultPersonalProfileDto = GetPersonalProfile();
    string serializedProfile = JsonSerializer.Serialize(defaultPersonalProfileDto);

    using (TestServer server = new TestServer(webHostBuilder))
    using (HttpClient client = server.CreateClient())
    {
        var result = await client.PostAsync("/api/PerfilPersonal/returnonlyid",
            new StringContent(serializedProfile, Encoding.UTF8, "application/json"));

        result.EnsureSuccessStatusCode();
    }
}

 

When validating, it is very common to just call result.EnsureSuccessStatusCode() to make sure the call worked as expected, but if you want, you can deserialize the content as we saw in the previous example. 

 

 

4 - TestServer with .NET6 or minimal APIs

Before finishing, I have to make a special mention to the case of .NET 6; This is because Microsoft has completely changed how web projects work with the arrival of minimal APIs; Now we no longer have the startup class with the ConfigureServices method, which was used by .UseStartup<T> to define what needed to be configured. 

 

In fact, the program.cs class is not visible from outside the project itself; instead, the compiler creates it. So, how do we run these tests?

 

The first thing we have to do is make the program class visible, and for this, we have two options:

The first is to modify the csproj of our API in net 6 to include that elements with internal access modifiers are visible to our test project:

<ItemGroup>
     <InternalsVisibleTo Include="WebPersonal.Backend.ApiTest" />
</ItemGroup>

 

And the second option is to create a partial class for program in the same program.cs file.

This is because the one created by the compiler is internal, and the one we create is public, so we can access it from the tests without modifying the csproj.

public partial class Program{}

The class does not need to contain anything, it just has to exist. 

 

 

Now, when creating the test, there are also two options. The first is to create a class that inherits from WebApplicationFactory<Program>, which will create a method called CreateHost where we can define the configuration:

class WebPersonalApi : WebApplicationFactory<Program>
{
    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder.ConfigureTestServices(serviceCollection =>
                {
                    serviceCollection
                        .AddScoped<IPostPersonalProfileDependencies, StubIPostPersonalProfileDependencies>();
                });
        return base.CreateHost(builder);
    }
}

And to call it from our tests is also very simple:

WebPersonalApi application = new ();
HttpClient client = application.CreateClient();
AcademicProjectsDto result = await client.GetFromJsonAsync<AcademicProjectsDto>("/api/AcademicProjects/1",
            new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });

 

The other option is to invoke within the test the WebApplicationFactory<Program> class directly: 

var application = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
    builder.ConfigureTestServices(serviceCollection =>
    {
        serviceCollection
            .AddScoped<IPostPersonalProfileDependencies, StubIPostPersonalProfileDependencies>();
    });
});
HttpClient client = application.CreateClient();
AcademicProjectsDto result = await client.GetFromJsonAsync<AcademicProjectsDto>("/api/AcademicProjects/1",
            new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });

 

Conclusion

In this post, we've seen how to create in-memory integration tests with TestServer

We've seen how to configure TestServer

How to add dependencies to TestServer

And how to use TestServer with .NET 6

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café