Integration Testing in C# — Complete Guide
In this tutorial, you will learn about Integration Testing in C#. We cover key concepts, practical examples, and best practices to help you master this topic.
Hook
Unit tests verify individual components in isolation, but integration tests confirm that your application works as a whole. C# developers use WebApplicationFactory to spin up real HTTP servers in tests, execute requests, and verify the complete request-response cycle against databases and external services.
Learning Path
graph LR A[Integration Testing] --> B[WebApplicationFactory] A --> C[TestContainers] B --> D[Custom Web Host] B --> E[In-Memory DB] C --> F[Docker Dependencies] style A fill:#4a90d9,color:#fff style B fill:#4a90d9,color:#fff style C fill:#4a90d9,color:#fff style D fill:#4a90d9,color:#fff style E fill:#4a90d9,color:#fff style F fill:#4a90d9,color:#fff
WebApplicationFactory
The WebApplicationFactory class creates a test server that hosts your application in memory.
// Install: dotnet add package Microsoft.AspNetCore.Mvc.Testing
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class BasicIntegrationTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Get_Endpoint_ReturnsSuccess()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/books");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
}
[Fact]
public async Task Get_Endpoint_ReturnsExpectedData()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/books/1");
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Laptop", content);
}
}
Customizing the Web Host
Override services in the test to use test-specific implementations.
public class CustomWebApplicationFactory
: WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the real DbContext registration
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
services.Remove(descriptor);
// Add in-memory database for testing
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
// Remove real email service
services.RemoveAll<IEmailService>();
services.AddScoped<IEmailService, MockEmailService>();
});
builder.UseEnvironment("Testing");
}
}
public class MockEmailService : IEmailService
{
public List<(string To, string Subject)> SentEmails { get; } = new();
public Task SendEmailAsync(string to, string subject, string body)
{
SentEmails.Add((to, subject));
return Task.CompletedTask;
}
}
Integration Test with Database
Test the full stack with an in-memory database.
public class BookApiTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory _factory;
public BookApiTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task CreateBook_ShouldPersistAndReturnCreated()
{
var newBook = new { Title = "Integration Testing with C#", Author = "Jane Doe", Price = 39.99m };
var json = JsonSerializer.Serialize(newBook);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/api/books", content);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var responseBody = await response.Content.ReadAsStringAsync();
var created = JsonSerializer.Deserialize<Book>(responseBody,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Assert.NotNull(created);
Assert.Equal("Integration Testing with C#", created.Title);
Assert.True(created.Id > 0);
// Verify it was actually persisted
var getResponse = await _client.GetAsync($"/api/books/{created.Id}");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
}
[Fact]
public async Task GetNonExistentBook_ShouldReturn404()
{
var response = await _client.GetAsync("/api/books/99999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
TestContainers
For true integration tests, use TestContainers to spin up Docker containers.
// Install: dotnet add package TestContainers.Containers
// dotnet add package TestContainers.MsSql
using Testcontainers.MsSql;
public class DatabaseIntegrationTests : IAsyncLifetime
{
private MsSqlContainer _msSqlContainer = null!;
public async Task InitializeAsync()
{
_msSqlContainer = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithPassword("Test_Password123!")
.Build();
await _msSqlContainer.StartAsync();
}
[Fact]
public async Task DatabaseConnection_ShouldWork()
{
var connectionString = _msSqlContainer.GetConnectionString();
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = "SELECT 1";
var result = await command.ExecuteScalarAsync();
Assert.Equal(1, result);
}
public async Task DisposeAsync()
{
await _msSqlContainer.DisposeAsync();
}
}
Testing Authentication
Test endpoints that require authentication.
public class AuthenticatedTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public AuthenticatedTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
private HttpClient CreateAuthenticatedClient()
{
var client = _factory.CreateClient();
// Generate a test JWT token
var token = GenerateTestToken("admin", new[] { "admin", "user" });
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return client;
}
private string GenerateTestToken(string username, string[] roles)
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, username)
};
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("test-secret-key-at-least-16-chars!"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "test-issuer",
audience: "test-audience",
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
[Fact]
public async Task AdminEndpoint_ShouldReturnOk_ForAdminUser()
{
var client = CreateAuthenticatedClient();
var response = await client.GetAsync("/api/admin/dashboard");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
Common Mistakes
Using production databases in tests: Always use a separate database (in-memory, TestContainers, or dedicated test instance). Never point tests at production.
Not cleaning up test data: Use database transactions or
EnsureDeleted()between tests to prevent data leakage between test cases.Ignoring test ordering issues: Tests should be independent and run in any order. Avoid shared mutable state.
Testing through the UI only: Integration tests at the HTTP API level are faster and more reliable than UI-level end-to-end tests.
Missing assertions on response content: Status code checks are not enough. Validate response bodies, headers, and side effects.
Practice Questions
Write an integration test that creates a user, authenticates, and accesses a protected resource.
Create a custom
WebApplicationFactorythat overrides the authentication handler to always authenticate as a test user.Write integration tests for a file upload endpoint that verifies file storage and metadata.
Challenge: Build a test suite that uses TestContainers for PostgreSQL and runs Migration scripts before each test class.
FAQ
Mini Project: Full API Integration Test Suite
Write comprehensive integration tests for a book management API.
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class BookApiIntegrationTests
: IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
private AppDbContext _context = null!;
public BookApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<AppDbContext>();
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));
});
});
_client = _factory.CreateClient();
}
public async Task InitializeAsync()
{
_context = _factory.Services.GetRequiredService<AppDbContext>();
// Seed data
_context.Books.Add(new Book { Id = 1, Title = "Test Book", Author = "Author", Price = 10m });
await _context.SaveChangesAsync();
}
[Fact]
public async Task GetAllBooks_ShouldReturnSeededData()
{
var books = await _client.GetFromJsonAsync<List<Book>>("/api/books");
Assert.NotNull(books);
Assert.Single(books);
}
[Fact]
public async Task CreateBook_WithInvalidData_ShouldReturn400()
{
var response = await _client.PostAsJsonAsync("/api/books", new { });
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task FullLifecycle_ShouldWork()
{
// Create
var createResponse = await _client.PostAsJsonAsync("/api/books",
new { Title = "New Book", Author = "Me", Price = 25m });
var created = await createResponse.Content.ReadFromJsonAsync<Book>();
Assert.NotNull(created);
// Read
var getResponse = await _client.GetAsync($"/api/books/{created.Id}");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
// Update
var updateResponse = await _client.PutAsJsonAsync($"/api/books/{created.Id}",
new { Id = created.Id, Title = "Updated", Author = "Me", Price = 30m });
Assert.Equal(HttpStatusCode.NoContent, updateResponse.StatusCode);
// Delete
var deleteResponse = await _client.DeleteAsync($"/api/books/{created.Id}");
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
// Verify gone
var getAgain = await _client.GetAsync($"/api/books/{created.Id}");
Assert.Equal(HttpStatusCode.NotFound, getAgain.StatusCode);
}
public async Task DisposeAsync()
{
await _context.DisposeAsync();
_client.Dispose();
}
}
Integration testing with C# and ASP.NET Core gives you confidence that your entire application works correctly. The WebApplicationFactory makes it easy to spin up test hosts, override dependencies, and verify the complete request lifecycle in your .NET applications.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro