.NET Integration Testing
Introduction
Integration testing is a crucial phase in software testing where individual components or modules are combined and tested as a group. Unlike unit tests that focus on isolated pieces of code, integration tests verify that different parts of your application work together correctly.
In the .NET ecosystem, integration testing helps ensure that your application's components—such as databases, file systems, APIs, and services—interact properly. This is especially important in modern applications that often depend on multiple systems working in harmony.
Why Integration Testing Matters
While unit tests are valuable for verifying individual components, they don't capture issues that arise when these components interact. Integration tests help identify:
- Interface defects between components
- Communication problems between services
- Issues with external dependencies like databases or APIs
- Configuration problems in your application environment
Setting Up Integration Tests in .NET
Let's start by setting up an integration testing project for a typical .NET application.
Creating a Test Project
// Assuming you have a solution with a main project called "MyWebApp"
// Create a new test project named "MyWebApp.IntegrationTests"
Project Structure
A common structure for integration tests in a .NET solution:
MySolution/
├── src/
│ └── MyWebApp/
└── tests/
├── MyWebApp.UnitTests/
└── MyWebApp.IntegrationTests/
Required NuGet Packages
For ASP.NET Core applications, you'll typically need:
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" />
</ItemGroup>
Testing Web APIs with WebApplicationFactory
ASP.NET Core provides the WebApplicationFactory
class, which makes it easy to set up integration tests for web applications.
Basic API Test Example
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace MyWebApp.IntegrationTests
{
public class BasicApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task GetProducts_ReturnsSuccessStatusCode()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}
What's Happening:
- We create a test class that uses
WebApplicationFactory<Program>
(for .NET 6+ projects) - The factory creates a test server hosting your application
- We use an HTTP client to make requests to our API endpoints
- We can test the responses to ensure they behave as expected
Testing with In-Memory Databases
For database integration tests, the Entity Framework Core in-memory provider is helpful:
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Xunit;
namespace MyWebApp.IntegrationTests
{
public class DatabaseTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public DatabaseTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add ApplicationDbContext using an in-memory database for testing
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
});
});
}
[Fact]
public async Task CreateProduct_AddsToDatabase()
{
// Arrange
var client = _factory.CreateClient();
var newProduct = new { Name = "Test Product", Price = 19.99 };
// Act
var response = await client.PostAsJsonAsync("/api/products", newProduct);
// Assert
response.EnsureSuccessStatusCode();
// Verify the product was added to the database
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var product = await dbContext.Products.FirstOrDefaultAsync(p => p.Name == "Test Product");
Assert.NotNull(product);
Assert.Equal(19.99, product.Price);
}
}
}
}
This example:
- Configures a custom
WebApplicationFactory
that replaces the real database with an in-memory one - Tests that our API endpoint properly adds a product to the database
- Verifies the result by checking the database content directly
Testing with Real Dependencies
Sometimes you need to test with actual dependencies like databases. For these scenarios, you can use Docker containers.
Using TestContainers for .NET
Testcontainers for .NET is a library that helps you run Docker containers for integration tests:
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using Xunit;
namespace MyWebApp.IntegrationTests
{
public class PostgresDatabaseTests : IAsyncLifetime
{
private readonly TestcontainersContainer _dbContainer;
private WebApplicationFactory<Program> _factory;
public PostgresDatabaseTests()
{
// Create a PostgreSQL container
_dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:13")
.WithEnvironment("POSTGRES_USER", "test")
.WithEnvironment("POSTGRES_PASSWORD", "test")
.WithEnvironment("POSTGRES_DB", "testdb")
.WithPortBinding(5432, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
.Build();
}
public async Task InitializeAsync()
{
// Start the container before tests run
await _dbContainer.StartAsync();
string connectionString = $"Host=localhost;Port={_dbContainer.GetMappedPublicPort(5432)};Database=testdb;Username=test;Password=test";
// Configure the factory with the real PostgreSQL database
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString);
});
});
});
}
public async Task DisposeAsync()
{
// Clean up after tests
await _dbContainer.StopAsync();
_factory?.Dispose();
}
[Fact]
public async Task DatabaseOperations_WorkWithRealDatabase()
{
// Arrange
var client = _factory.CreateClient();
// Act & Assert - test your application with a real PostgreSQL database
// ...
}
}
}
This approach:
- Spins up a real PostgreSQL database in a Docker container
- Configures your application to use this database for tests
- Cleans up after tests are complete
Testing Communication Between Microservices
In microservice architectures, testing service-to-service communication is vital:
[Fact]
public async Task OrderService_CommunicatesWithPaymentService()
{
// Arrange
var client = _factory.CreateClient();
var order = new
{
CustomerId = "customer-123",
Items = new[]
{
new { ProductId = "product-1", Quantity = 2 }
},
TotalAmount = 39.98
};
// Act
var response = await client.PostAsJsonAsync("/api/orders", order);
// Assert
response.EnsureSuccessStatusCode();
var orderResult = await response.Content.ReadFromJsonAsync<OrderResult>();
// Verify that a payment was initiated
using (var scope = _factory.Services.CreateScope())
{
var paymentRepository = scope.ServiceProvider.GetRequiredService<IPaymentRepository>();
var payment = await paymentRepository.GetByOrderIdAsync(orderResult.OrderId);
Assert.NotNull(payment);
Assert.Equal(39.98, payment.Amount);
Assert.Equal("Pending", payment.Status);
}
}
Mocking External Services
Sometimes you need to mock external services for testing:
public class ExternalServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ExternalServiceTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace the real service with a mock
services.AddScoped<IWeatherService, MockWeatherService>();
});
});
}
[Fact]
public async Task WeatherForecast_ReturnsExpectedResults()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/weather");
// Assert
response.EnsureSuccessStatusCode();
var forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>();
Assert.NotNull(forecasts);
Assert.Equal(5, forecasts.Length);
// Further assertions based on the mock service's expected behavior
}
// Mock implementation of the external service
private class MockWeatherService : IWeatherService
{
public Task<WeatherForecast[]> GetForecastAsync()
{
// Return predictable test data
return Task.FromResult(new[]
{
new WeatherForecast { Date = DateTime.Now, Temperature = 22, Summary = "Mild" },
// Additional mock forecasts...
});
}
}
}
Best Practices for Integration Testing
1. Keep Tests Independent
Each test should run independently of others. Don't create tests that depend on the state left by previous tests.
[Fact]
public async Task GetProducts_AfterAddingProduct_ReturnsProducts()
{
// Arrange - Reset the database or use a unique database for this test
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
// Add test data
dbContext.Products.Add(new Product { Name = "Test Product", Price = 19.99m });
await dbContext.SaveChangesAsync();
}
// Act
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
var products = await response.Content.ReadFromJsonAsync<List<Product>>();
Assert.Single(products);
}
2. Use Test Categories
Organize tests by type to run them separately:
[Fact]
[Trait("Category", "Integration")]
public async Task Database_Integration_Test()
{
// Test code
}
[Fact]
[Trait("Category", "API")]
public async Task API_Integration_Test()
{
// Test code
}
3. Handle Authentication
If your API requires authentication, set up test users or mock the authentication:
[Fact]
public async Task SecureEndpoint_WithValidToken_ReturnsData()
{
// Arrange
var client = _factory.CreateClient();
// Add authentication token
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
GenerateTestToken());
// Act
var response = await client.GetAsync("/api/secure-data");
// Assert
response.EnsureSuccessStatusCode();
}
private string GenerateTestToken()
{
// Generate a test JWT token
// (Implementation depends on your authentication setup)
}
Common Integration Testing Scenarios
1. Testing API Endpoints
[Fact]
public async Task GetProducts_WithFiltering_ReturnsFilteredProducts()
{
// Arrange
var client = _factory.CreateClient();
// Seed the database with test data
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Products.AddRange(
new Product { Name = "Laptop", Category = "Electronics", Price = 999.99m },
new Product { Name = "Headphones", Category = "Electronics", Price = 99.99m },
new Product { Name = "Book", Category = "Books", Price = 19.99m }
);
await dbContext.SaveChangesAsync();
}
// Act
var response = await client.GetAsync("/api/products?category=Electronics");
// Assert
response.EnsureSuccessStatusCode();
var products = await response.Content.ReadFromJsonAsync<List<Product>>();
Assert.Equal(2, products.Count);
Assert.All(products, p => Assert.Equal("Electronics", p.Category));
}
2. Testing Error Handling
[Fact]
public async Task GetProduct_WithInvalidId_Returns404()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/products/999"); // Non-existent ID
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
3. Testing Database Transactions
[Fact]
public async Task PlaceOrder_WithInsufficientInventory_RollsBackTransaction()
{
// Arrange
var client = _factory.CreateClient();
// Seed the database
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Products.Add(new Product
{
Id = 1,
Name = "Limited Product",
Price = 99.99m,
InventoryCount = 1 // Only one in stock
});
await dbContext.SaveChangesAsync();
}
// Act - Try to order 2 of the product
var orderRequest = new { ProductId = 1, Quantity = 2 };
var response = await client.PostAsJsonAsync("/api/orders", orderRequest);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
// Verify inventory wasn't changed
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var product = await dbContext.Products.FindAsync(1);
Assert.Equal(1, product.InventoryCount); // Still has 1 in stock
}
}
Summary
Integration testing is an essential practice in .NET development that helps ensure your application's components work together as expected. By testing interactions between services, databases, and external systems, you can catch issues that unit tests might miss.
Key takeaways:
- Integration tests verify that different parts of your application work together correctly
- The
WebApplicationFactory
class makes testing ASP.NET Core applications straightforward - You can test with in-memory databases or real databases using containers
- Proper test independence and setup/teardown are crucial for reliable tests
- Integration tests complement unit tests but don't replace them
Additional Resources
- Microsoft's documentation on integration testing
- Introduction to Testcontainers
- EF Core testing best practices
- xUnit documentation
Exercises
- Create an integration test for a simple API that creates and retrieves users
- Write a test that verifies database constraints are enforced (e.g., unique email addresses)
- Test a workflow that spans multiple API endpoints (e.g., create a cart, add items, checkout)
- Implement a test that verifies error handling when external services fail
- Create a test that verifies authentication and authorization rules for your API endpoints
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)