Integration testing for dotnet core APIs: Introduction

Article Banner
Author(s): Ajay Kumar
Published On: 25 Jan 2025


This is going to be multi-part series on integration tests from introduction to advanced used cases.

What are integration tests in context of APIs?


In the context of .NET Core APIs for a blog, integration tests are automated tests that evaluate the functionality of various parts of your application working together as a whole. Specifically, these tests ensure that multiple components — such as controllers, database access, middleware, internal and external services — function correctly when integrated, as opposed to functioning correctly only in isolation (which would be covered by unit tests).

Here is a quick one-liner summary on popular kinds of testing for an API codebase:

  • Unit Tests: Validate individual components like controllers or services in isolation.
  • Integration Tests: Verify the interaction between multiple components (e.g., API, database, middleware) as a cohesive system.
  • Contract Tests: Ensure that API endpoints conform to agreed-upon interfaces or expectations between services.

Integration tests will help you in identifying possible bugs introduced due to any new changes in your code.

Build process when integration tests fail
Figure 1 : Build process when integration tests fail
Build process when integration tests pass
Figure 2 : Build process when integration tests pass

Ways to write integration tests


Now there are various ways to write integration tests:

  • Using Postman or any other API testing tool
  • Deploying services to actual environment, either server or cloud service
  • Using WebApplicationFactory in dotnet to run an in-memory server locally

We will be discussing the WebApplicationFactory method, which is fast, flexible, configurable and does not require any additional tools or hosting environment.

Some of the advantages of this approach are as below:

  • Realistic Environment: It provides a full-fledged, in-memory test server that mimics the real production environment, allowing you to test the entire request/response pipeline of your application without needing to host it on an actual web server.
  • End-to-End Testing: You can perform end-to-end testing, including routing, middleware, dependency injection, and database interactions, ensuring that all components work together as expected.
  • Customizable Configuration: You can override or customize the application’s configuration (e.g., swapping real services or databases for test versions) to simulate different environments or conditions without affecting the production code.
  • Easy HTTP Client Access: WebApplicationFactory makes it simple to create an HttpClient for sending HTTP requests to your API, making it easy to test API endpoints and validate their responses.
  • Automatic Startup: It handles the application’s startup process, so you don’t have to manually configure or boot the application, saving time and reducing boilerplate code.
  • Flexible Testing with Dependency Injection: You can easily replace services, middleware, or database contexts using the test server’s dependency injection, which allows for testing specific scenarios like using an in-memory database.
  • Seamless Integration with Test Frameworks: It integrates well with popular testing frameworks like xUnit, NUnit, or MSTest, and is optimized for use in .NET Core testing scenarios, reducing friction in writing and running tests.

Diving into the code


For a demo code, we will be referring a code repository, which stores the information for superhero personalities. I’ll be sharing the GitHub link of the code repo at the end of the article.

Our demo API has 2 APIs:

Demo API swagger definition
Figure 3 : Demo API swagger definition

A quick glimpse on our simple SuperHero controller


[ApiController]
[Route("[controller]")]
public class SuperHeroController(ISuperHeroRepository superHeroRepository)
    : ControllerBase
{
    [HttpGet("")]
    public async Task<IEnumerable<SuperHero>> Get()
    {
        return await superHeroRepository.GetAllSuperHeroes();
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var superHero = await superHeroRepository.GetSuperHeroById(id);
        if (superHero == null)
        {
            return NotFound();
        }

        return Ok(superHero);
    }
}
        
Code Sample #1 : APIs for SuperHero

Since this is basic demo, our ISuperHeroRepository interacts with a Sqlite database which has some predefined SuperHero entries.

Integration test example


Here is an example for our integration test for Get SuperHero By Id scenario:


[Fact(DisplayName = "Get superhero by Id returns superhero")]
public async Task Get_ById_SuperHero_Returns_SuperHero()
{
    // Arrange
    var factory = new WebApplicationFactory<Program>();
    var htmlClient = factory.CreateClient();
    
    // Act
    var response = await htmlClient.GetAsync("/SuperHero/1");

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);
    var superHeroes = await response.Content.ReadFromJsonAsync<SuperHero>();
    superHeroes.Should().NotBeNull();
    superHeroes!.Id.Should().Be(1);
    superHeroes!.SuperName.Should().Be("Batman");
}
        
Code Sample #2 : Integration test for Get SuperHero By Id

Let’s understand what’s happening inside the test:

1. Test Definition and Setup

The test starts with the [Fact] attribute from xUnit, which defines the test and makes it executable within the test framework. The DisplayName parameter provides a readable description for the test, which is helpful for test reports.


        [Fact(DisplayName = "Get superhero by Id returns superhero")]
        
Code Sample #3 : Test Definition

This test will ensure that the API correctly retrieves a superhero by ID when we hit the /SuperHero/1 endpoint.

2. Arranging the Test Environment

In the Arrange phase, we use WebApplicationFactory to spin up a test server and create an HttpClient for sending requests to the API.


        var factory = new WebApplicationFactory<Program>();
        var httpClient = factory.CreateClient();
        
Code Sample #4 : Arrange Phase

  • WebApplicationFactory<Program>: This initializes a test instance of the API, emulating the real application.
  • CreateClient(): This method returns an HttpClient that can be used to make requests to the in-memory test server.

By using WebApplicationFactory, we simulate the application environment, allowing us to test the full HTTP pipeline, including routing, middleware, and controllers.

3. Act: Sending the Request

In the Act phase, we make a GET request to the /SuperHero/1 endpoint using the httpClient created earlier.


        var response = await httpClient.GetAsync("/SuperHero/1");
        
Code Sample #5 : Act Phase

This line sends an HTTP GET request to the API and awaits the response. The application processes the request as it would in production, returning the corresponding superhero (with ID 1) if it exists.

4. Assert: Validating the Response

Now we enter the Assert phase, where we check if the response from the API matches our expectations.

Checking the Status Code:


        response.StatusCode.Should().Be(HttpStatusCode.OK);
        
Code Sample #6 : Assert Status Code

This assertion ensures the API returns a status code of 200 OK, confirming that the request was successful. If the API returned any other status code (like 404 Not Found or 500 Internal Server Error), the test would fail.

Validating the Returned Data


Next, we check the actual content of the response by deserializing the JSON response into a SuperHero object:


        var superHero = await response.Content.ReadFromJsonAsync<SuperHero>();
        superHero.Should().NotBeNull();
        
Code Sample #7 : Validate Returned Data

  • ReadFromJsonAsync<SuperHero>(): This method reads the JSON response and converts it into a SuperHero object. If the response doesn’t match the expected structure, or if the superhero doesn't exist, the test will fail.
  • superHero.Should().NotBeNull(): This assertion checks that the API did, in fact, return a superhero. If the API returned null (i.e., no superhero was found), the test would fail here.

Verifying Specific Property Values


Finally, we verify that the returned superhero has the correct properties, specifically checking that the ID is 1 and the superhero's name is "Batman":


        superHero!.Id.Should().Be(1);
        superHero!.SuperName.Should().Be("Batman");
        
Code Sample #8 : Verify Specific Property Values

When executed, if everything is good with our code, we get all green tests.

Success test result
Figure 4 : Success test result

Negative Scenario


Let’s deliberately try to break our code to understand how the test behaves in a negative scenario. The test scenario Get superhero by invalid Id returns not found expects a 404 Not Found status code when a superhero is not found.

We will make a change in our code to return 400 Bad Request instead of 404 Not Found:


        [HttpGet("{id}")]
        public async Task<IActionResult> GetById(int id)
        {
            var superHero = await superHeroRepository.GetSuperHeroById(id);
            if (superHero == null)
            {
                return BadRequest(); // Changed from NotFound()
            }

            return Ok(superHero);
        }
        
Code Sample #9 : Modified GetById Method

Now, let’s run the test and observe the results. The test result is as follows:


        Expected response.StatusCode to be HttpStatusCode.NotFound {value: 404}, 
        but found HttpStatusCode.BadRequest {value: 400}.
        
Code Sample #10 : Test Failure Result

Failure result status
Figure 5 : Failure result status

This failure indicates that the test correctly identified the mismatch between the expected and actual status codes. The test expected a 404 Not Found, but the API returned a 400 Bad Request due to the deliberate change in the code.

Such scenarios highlight the importance of integration tests in catching unintended changes or bugs in the application behavior.

This is it for the basic setup demo. I will be covering more in the future articles like, how to work with database, authentication, events etc.

For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/pact-net-example

Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com

Copyright © 2025 Dev Codex

An unhandled error has occurred. Reload 🗙