Integration testing for dotnet core APIs: Introduction

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.


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:

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 anHttpClient
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 aSuperHero
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.

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

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