Integration Testing in ASP.NET Core Web API

Integration Testing in ASP.NET Core Web API

In this article, I will discuss Integration Testing in ASP.NET Core Web API with Examples. Please read our Real-time ASP.NET Core Project Development for Unit Testing article before proceeding to this article, where we discuss the development of a real-time application. We will use the same application to implement Integration Testing in ASP.NET Core Web API.

What Is Integration Testing?

Integration testing is a type of software testing that combines multiple components, modules, or systems to ensure they function correctly when working together. While unit tests verify the smallest components of your application (such as individual methods), integration tests ensure that different parts of your system work together as expected.

In the context of ASP.NET Core Web API, this means testing the API endpoints, their interaction with the business logic, data access layers, and even external dependencies (like databases or services). For example, in a Web API, integration testing verifies that the HTTP endpoints correctly receive requests, interact with the business logic and database, and return the expected responses. For a better understanding, please refer to the following image.

What Is Integration Testing?

Here,

  • Left side – individual components (controller, service, repository) that would be tested in isolation by unit tests.
  • Right side – an integration test spins up the real HTTP pipeline, exercises the endpoint with a real HttpClient, flows through middleware → controller → service → database, and checks the final response.
Examples to Understand Integration Testing

In the following examples, integration testing ensures that all parts of the system communicate and collaborate properly to deliver the final outcome.

Online Shopping:

Examples to Understand Integration Testing

Here,

  • Unit Testing: Verify that the payment processor accurately calculates taxes on individual transactions.
  • Integration Testing: Validate the entire checkout process, from adding products to the cart, calculating the total price, processing the payment, and updating the inventory, ensuring all components interact correctly.
Coffee Shop:

Integration Testing in ASP.NET Core Web API

Here,

  • Unit Testing: Verify that the coffee machine operates correctly independently.
  • Integration Testing: Test the complete order process where the customer places an order with the cashier, the coffee machine prepares the drink, and the serving staff delivers it, ensuring all these parts coordinate properly to serve the customer.
Why Do We Need Integration Testing in ASP.NET Core Web API?

Why Do We Need Integration Testing in ASP.NET Core Web API?

With Integration Testing in ASP.NET Core Web API, we will get the following benefits:

  • Validate End-to-End Behaviour: Integration tests ensure that the API endpoints correctly integrate with databases, services, and other dependencies, verifying realistic scenarios beyond isolated units.
  • Catch Issues Missed by Unit Tests: While unit tests focus on logic in isolation, integration tests catch bugs caused by incorrect configuration, communication errors, or data mismatch between layers.
  • Confidence in Deployment: With integration tests, developers can be confident that the whole system behaves as expected before deploying to production.
  • Test Realistic Workflows: Many bugs appear only when different components work together, e.g., incorrect serialization, authentication issues, or database schema mismatches.
  • Prevent Regression: Detects if new changes break existing functionality when different modules work together.
  • Database and Transaction Boundaries: Ensures EF Core mappings, migrations, and repositories behave correctly with actual SQL (or at least with an in-memory database that honours constraints).
How Do We Implement Integration Testing in ASP.NET Core Web API?

We need to follow the steps below to implement Integration Testing in ASP.NET Core Web API:

  1. Create an Integration Test Project: Typically, a separate xUnit or NUnit test project. Add references to the Web API project and testing packages.
  2. Use the Microsoft.AspNetCore.Mvc.Testing package: This package provides infrastructure for integration testing ASP.NET Core applications, including a WebApplicationFactory<TEntryPoint> class to create an in-memory test server.
  3. Use WebApplicationFactory to create a Test Server: WebApplicationFactory hosts your API in-memory. You can send HTTP requests to this in-memory server as if calling a live API.
  4. Use HttpClient to send requests: Use the HttpClient obtained from WebApplicationFactory to call API endpoints. Assert the HTTP status codes, headers, and response body.
  5. Handle Dependencies (Database, Services): Use an In-Memory Database (e.g., InMemory provider for Entity Framework Core) or a test-specific database. Override services in the Program class using ConfigureTestServices for dependency injection, allowing you to mock external services or replace configurations.
Create an Integration Test Project

We need a separate project in our solution for integration tests, so our tests don’t mix with our main API code. This project will simulate real client interactions with our API. xUnit is a widely used testing framework for .NET Core with great support and integration.

In Visual Studio:

  • Right-click your Solution → Add → New Project.
  • Select xUnit Test Project.
  • Name it ProductCatalog.IntegrationTests (or any meaningful name).
  • Click Create.
Add Project Reference

The test project needs to see our API’s controllers, models, and startup code. This allows the test project to access your API’s classes, DTOs, and services. Add a Project Reference to our Main API project from the Integration Project. To do so,

  • Right-click on the IntegrationTests project → Add → Project Reference.
  • Select your main Web API project (ProductCatalog.API).
Add NuGet Packages

The following packages provide tools to spin up your API in-memory, use EF Core’s InMemory provider, and run xUnit tests. In Visual Studio, open the Package Manager Console and ensure the Default project dropdown is set to ProductCatalog.IntegrationTests and then execute the following commands.

  • Install-Package Microsoft.AspNetCore.Mvc.Testing
  • Install-Package Microsoft.EntityFrameworkCore.InMemory
What do they do?
  • Microsoft.AspNetCore.Mvc.Testing: Provides WebApplicationFactory<TEntryPoint>, which hosts our API in-memory for testing (no network, no port needed).
  • Microsoft.EntityFrameworkCore.InMemory: Enables an in-memory database provider for Entity Framework Core, allowing fast, isolated database testing without an actual SQL Server instance.
Configure the WebApplicationFactory

The WebApplicationFactory<TEntryPoint> spins up your entire ASP.NET Core Web API inside a test server, no HTTP port, no IIS/Express, just a real pipeline and DI container, all in memory. This also allows us to send real HTTP requests using HttpClient as if you were an external consumer.

Why Customize It?
  • By default, our API connects to the real database.
  • For testing, we want to replace it with an in-memory database that is fast, isolated, and resets every time it is run.
  • We may also want to seed test data so our tests have something predictable to work with.
How To Implement:

Create a class file named CustomWebApplicationFactory.cs in your integration test project and copy and paste the following code. The primary objective is to replace the production SQL Server database registration with an in-memory database for faster, isolated testing. The following code is self-explained, so please read the comment lines for a better understanding.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ProductCatalog.API;
using ProductCatalog.API.Data;

namespace ProductCatalog.IntegrationTests
{
    // Custom WebApplicationFactory spins up the ASP.NET Core app in-memory for integration testing.
    // Inheriting from WebApplicationFactory<Program> allows overriding the startup configuration for tests.
    public class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        // Override ConfigureWebHost to customize the test server's service registrations.
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            // ConfigureServices is used to modify the Dependency Injection (DI) container.
            builder.ConfigureServices(services =>
            {
                // Locate the existing DbContext registration in the service collection.
                // This is usually registered with SQL Server in the main app, which we want to replace.
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));

                // If a registration for ApplicationDbContext exists, remove it.
                // This prevents conflicts between the real SQL Server provider and InMemory provider.
                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }

                // Create a new ServiceProvider that includes the EF Core InMemory database services.
                // This service provider will be shared internally by all DbContext instances to ensure they use the same in-memory database.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Register ApplicationDbContext with the InMemory database provider.
                // The database is named "IntegrationTestDb" to create a shared in-memory database instance.
                // Using UseInternalServiceProvider ensures all DbContext instances share the same internal EF services and in-memory store.
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("IntegrationTestDb");
                    options.UseInternalServiceProvider(serviceProvider);
                });

                // Build the complete service provider from the updated services collection.
                // This service provider is used to create scopes and resolve services.
                var sp = services.BuildServiceProvider();

                // Create a scope to obtain scoped services (like DbContext) for database initialization.
                using (var scope = sp.CreateScope())
                {
                    // Get the scoped service provider for resolving dependencies.
                    var scopedServices = scope.ServiceProvider;

                    // Resolve the ApplicationDbContext instance from the DI container.
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();

                    // Ensure the in-memory database is created. This sets up the schema based on the EF Core model.
                    db.Database.EnsureCreated();

                    // Seed initial test data into the database to guarantee known state before tests run.
                    // Utilities.InitializeDbForTests contains methods to insert products, customers, etc.
                    Utilities.InitializeDbForTests(db);
                }
            });
        }
    }
}
Why this setup?
  • Removes SQL Server registration: So, we don’t have two providers (SQL + InMemory) and get exceptions.
  • Uses InMemory DB: Fast, no external dependency.
  • Shared ServiceProvider: Ensures that all DbContext instances share the same in-memory store, making data visible.
  • Seed Test Data: Every test begins with a predictable database.
Seed Test Data

Integration tests must be predictable. If the test database is empty, your API will likely return a 404 error or other errors. Seeding ensures each test has known Customers, Products, etc. So, create a class file named Utilities.cs in the Integration Test project and copy and paste the following code. The InitializeDbForTests method is called inside CustomWebApplicationFactory to seed the in-memory DB before running tests.

using ProductCatalog.API.Data;
using ProductCatalog.API.Models;

namespace ProductCatalog.IntegrationTests
{
    public static class Utilities
    {
        // This method seeds the in-memory database with test data for integration tests.
        // It ensures the database is in a clean, known state before each test run.
        public static void InitializeDbForTests(ApplicationDbContext db)
        {
            // Remove all existing data from Products table to avoid duplicate key errors and stale data
            db.Products.RemoveRange(db.Products);

            // Remove all existing data from Customers table for a clean slate
            db.Customers.RemoveRange(db.Customers);

            // Remove all existing data from Orders table to prevent foreign key conflicts
            db.Orders.RemoveRange(db.Orders);

            // Remove all existing data from OrderItems table (child of Orders) to ensure consistency
            db.OrderItems.RemoveRange(db.OrderItems);

            // Persist all deletions to the in-memory database
            db.SaveChanges();

            // Add sample Product records to the database with fixed Ids and test values
            db.Products.AddRange(
                new Product { Id = 1, Name = "Test Laptop", Price = 60000m, Stock = 20 },
                new Product { Id = 2, Name = "Test Smartphone", Price = 25000m, Stock = 50 }
            );

            // Add a sample Customer record with a known Id and test email for test scenarios
            db.Customers.AddRange(
                new Customer { Id = 1, Name = "Test Customer", Email = "test@example.com" }
            );

            // Persist these inserted test records to the in-memory database
            db.SaveChanges();
        }
    }
}
Code Explanations:
  • Why remove existing data first: To ensure test isolation and prevent data conflicts or contamination between tests.
  • Order of removals: Remove dependent child data (OrderItems) before parent data (Orders) to respect foreign key constraints even in-memory.
  • Seeding fixed IDs: Allows tests to refer to known entities (CustomerId = 1, ProductId = 1) for predictable queries.
  • Saving changes: It is important to persist deletions and insertions so that subsequent operations see the updated data.
Writing Integration Tests

Now, with a seeded in-memory database and a real API pipeline, we can simulate client requests and validate real-world scenarios. So, create a new test class file named OrderIntegrationTests.cs within the Integration Tests Project and copy and paste the following code. This uses the HttpClient created by WebApplicationFactory to simulate actual HTTP requests to our API.

using System.Net.Http.Json;  
using ProductCatalog.API.DTOs;  

namespace ProductCatalog.IntegrationTests
{
    // Integration test class for testing the Orders API endpoints.
    // Uses the CustomWebApplicationFactory to create a test server and HttpClient for in-memory HTTP requests.
    public class OrderIntegrationTests : IClassFixture<CustomWebApplicationFactory>
    {
        private readonly HttpClient _client;  // HttpClient instance used to send HTTP requests to the in-memory API

        // Constructor receives the factory instance from xUnit via IClassFixture
        // Factory creates the in-memory test server and HttpClient
        public OrderIntegrationTests(CustomWebApplicationFactory factory)
        {
            // Creates an HttpClient configured to communicate with the test server
            _client = factory.CreateClient(); 
        }

        // Test case to verify that creating an order with valid data returns success and expected response
        [Fact]
        public async Task CreateOrder_ValidRequest_ReturnsSuccess()
        {
            // Arrange: Prepare a valid OrderCreateDTO with a known CustomerId and a list of order items
            var orderDto = new OrderCreateDTO
            {
                CustomerId = 1,  // Assumes this customer exists in seeded test data
                Items = new List<OrderItemDTO>
                {
                    new OrderItemDTO { ProductId = 1, Quantity = 2 }  // Order 2 units of ProductId 1
                }
            };

            // Act: Send a POST request to the /api/orders endpoint with the orderDto serialized as JSON
            var response = await _client.PostAsJsonAsync("/api/orders", orderDto);

            // Assert: Verify the response indicates a successful HTTP status code (200-299)
            response.EnsureSuccessStatusCode();

            // Deserialize the JSON response content into an OrderResponseDTO object
            var result = await response.Content.ReadFromJsonAsync<OrderResponseDTO>();

            // Assert: Validate the response DTO is not null and contains the expected CustomerId
            Assert.NotNull(result);
            Assert.Equal(orderDto.CustomerId, result.CustomerId);
        }

        // Test case to verify fetching an existing order returns the expected order details
        [Fact]
        public async Task GetOrder_ExistingOrder_ReturnsSuccess()
        {
            // Arrange: First, create an order explicitly to ensure there is an order to fetch later
            var orderDto = new OrderCreateDTO
            {
                CustomerId = 1,  // Use the seeded test customer
                Items = new List<OrderItemDTO>
                {
                    new OrderItemDTO { ProductId = 1, Quantity = 2 }
                }
            };

            // Send POST request to create the order and ensure success
            var createResponse = await _client.PostAsJsonAsync("/api/orders", orderDto);
            createResponse.EnsureSuccessStatusCode();

            // Deserialize the created order response to get the assigned OrderId
            var createdOrder = await createResponse.Content.ReadFromJsonAsync<OrderResponseDTO>();
            var orderId = createdOrder?.OrderId;  // Safely extract OrderId for the next request

            // Act: Send a GET request to fetch the order by its OrderId
            var response = await _client.GetAsync($"/api/orders/{orderId}");

            // Assert: Confirm the GET request succeeded
            response.EnsureSuccessStatusCode();

            // Deserialize the order details from the response
            var result = await response.Content.ReadFromJsonAsync<OrderResponseDTO>();

            // Assert: Validate the response is not null and the returned OrderId matches the requested one
            Assert.NotNull(result);
            Assert.Equal(orderId, result.OrderId);
        }
    }
}
Code Explanations:
  • The test class uses in-memory HTTP calls to simulate real client-server interaction.
  • CustomWebApplicationFactory creates the test server hosting the real API pipeline.
  • HttpClient is used to send HTTP requests with JSON payloads and read JSON responses.
  • The first test ensures the POST endpoint creates an order successfully.
  • The second test explicitly creates an order first, then fetches it to verify the GET endpoint.
  • Assertions ensure HTTP success status and expected data correctness.
Running Integration Tests

Run your tests via: Visual Studio Test Explorer: Open Test → Test Explorer → Run All Tests.

Integration testing is essential in modern API development, especially with ASP.NET Core Web API. It bridges the gap between isolated unit tests and the actual behavior of your application in a real environment. By writing integration tests, we increase confidence, catch bugs early, and ensure that our application works as intended when all its components come together.

By implementing integration tests using WebApplicationFactory, HttpClient, and test-friendly configurations (such as in-memory databases), developers gain confidence in the reliability and robustness of their APIs before production deployment.

In the next article, I will discuss how to implement Test-Driven Development (TDD) in an ASP.NET Core Web API application. In this article, I explain Integration Testing in an ASP.NET Core Web API. I hope you enjoy this article on Integration Testing in an ASP.NET Core Web API.

1 thought on “Integration Testing in ASP.NET Core Web API”

  1. blank

    🎥 Watch Now: Master Unit Testing Repositories in ASP.NET Core Web API!

    Are you looking to improve the quality and reliability of your ASP.NET Core Web API projects? Check out our latest video where Pranaya Rout explains step-by-step how to unit test repositories effectively using EF Core InMemory provider. This tutorial covers practical examples, best practices, and tips to write clean, maintainable tests for your data access layer.

    👉 Don’t miss this essential guide to boost your .NET development skills!
    Watch the full video here: https://www.youtube.com/watch?v=qCziqSpvmGE

    Happy coding with Dot Net Tutorials!

Leave a Reply

Your email address will not be published. Required fields are marked *