Integration Testing
Integration testing verifies that different components or systems work correctly together. In .NET, this typically involves testing controllers, databases, external services, and the complete request-response pipeline.
Official Definition/Standards
Integration testing is a software testing technique where individual software modules are combined and tested as a group. In ASP.NET Core, integration tests use the TestHost and WebApplicationFactory to test the entire HTTP request pipeline.
Setup and Usage (Tools, Packages, Test Runners)
Essential Packages:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Testcontainers # For real database testing
Test Infrastructure:
- WebApplicationFactory: Creates a test server for ASP.NET Core applications
- TestHost: Hosts the application in-memory for testing
- HttpClient: Makes HTTP requests to the test server
- In-Memory Database: EF Core provider for testing without real database
Typical Test Architecture and Patterns
Common Patterns:
- Test Fixtures: Shared test server setup
- Custom WebApplicationFactory: Override services for testing
- Database Seeding: Prepare test data
- Test Containers: Use real databases in Docker
- API Client Testing: Test HTTP endpoints end-to-end
Example Integration Test Code
// Package references:
// <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
// <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net.Http.Json;
using Xunit;
using FluentAssertions;
// Test startup configuration
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the real database
services.RemoveAll(typeof(DbContextOptions<HotelDbContext>));
// Add in-memory database
services.AddDbContext<HotelDbContext>(options =>
{
options.UseInMemoryDatabase("TestDatabase");
});
// Build service provider and seed database
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<HotelDbContext>();
SeedDatabase(context);
});
builder.UseEnvironment("Testing");
}
private static void SeedDatabase(HotelDbContext context)
{
context.Database.EnsureCreated();
// Seed test data
if (!context.Guests.Any())
{
context.Guests.AddRange(new[]
{
new Guest { Id = 1, FirstName = "John", LastName = "Doe", Email = "john@example.com" },
new Guest { Id = 2, FirstName = "Jane", LastName = "Smith", Email = "jane@example.com" }
});
}
if (!context.Rooms.Any())
{
context.Rooms.AddRange(new[]
{
new Room { Id = 101, RoomNumber = "101", RoomType = "Standard", PricePerNight = 99.99m, IsAvailable = true },
new Room { Id = 102, RoomNumber = "102", RoomType = "Deluxe", PricePerNight = 149.99m, IsAvailable = true }
});
}
context.SaveChanges();
}
}
// Integration test class
public class BookingControllerIntegrationTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program> _factory;
public BookingControllerIntegrationTests(CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task CreateBooking_WithValidData_ReturnsCreatedBooking()
{
// Arrange
var createRequest = new CreateBookingRequest
{
GuestId = 1,
RoomId = 101,
CheckInDate = DateTime.Today.AddDays(1),
CheckOutDate = DateTime.Today.AddDays(3),
TotalAmount = 199.98m
};
// Act
var response = await _client.PostAsJsonAsync("/api/bookings", createRequest);
// Assert
response.Should().HaveStatusCode(HttpStatusCode.Created);
var booking = await response.Content.ReadFromJsonAsync<Booking>();
booking.Should().NotBeNull();
booking!.GuestId.Should().Be(createRequest.GuestId);
booking.RoomId.Should().Be(createRequest.RoomId);
booking.TotalAmount.Should().Be(createRequest.TotalAmount);
// Verify location header
response.Headers.Location.Should().NotBeNull();
response.Headers.Location!.ToString().Should().Contain($"/api/bookings/{booking.Id}");
}
[Fact]
public async Task GetBooking_WithExistingId_ReturnsBooking()
{
// Arrange - Create a booking first
var createRequest = new CreateBookingRequest
{
GuestId = 1,
RoomId = 101,
CheckInDate = DateTime.Today.AddDays(1),
CheckOutDate = DateTime.Today.AddDays(3),
TotalAmount = 199.98m
};
var createResponse = await _client.PostAsJsonAsync("/api/bookings", createRequest);
var createdBooking = await createResponse.Content.ReadFromJsonAsync<Booking>();
// Act
var getResponse = await _client.GetAsync($"/api/bookings/{createdBooking!.Id}");
// Assert
getResponse.Should().HaveStatusCode(HttpStatusCode.OK);
var retrievedBooking = await getResponse.Content.ReadFromJsonAsync<Booking>();
retrievedBooking.Should().NotBeNull();
retrievedBooking!.Id.Should().Be(createdBooking.Id);
retrievedBooking.GuestId.Should().Be(createRequest.GuestId);
}
[Fact]
public async Task GetBooking_WithNonExistentId_ReturnsNotFound()
{
// Act
var response = await _client.GetAsync("/api/bookings/99999");
// Assert
response.Should().HaveStatusCode(HttpStatusCode.NotFound);
}
[Fact]
public async Task CreateBooking_WithInvalidData_ReturnsBadRequest()
{
// Arrange
var invalidRequest = new CreateBookingRequest
{
GuestId = 999, // Non-existent guest
RoomId = 101,
CheckInDate = DateTime.Today.AddDays(3),
CheckOutDate = DateTime.Today.AddDays(1), // Invalid dates
TotalAmount = -100m // Invalid amount
};
// Act
var response = await _client.PostAsJsonAsync("/api/bookings", invalidRequest);
// Assert
response.Should().HaveStatusCode(HttpStatusCode.BadRequest);
}
[Fact]
public async Task GetAllBookings_ReturnsBookingsList()
{
// Arrange - Create multiple bookings
var bookingRequests = new[]
{
new CreateBookingRequest
{
GuestId = 1,
RoomId = 101,
CheckInDate = DateTime.Today.AddDays(1),
CheckOutDate = DateTime.Today.AddDays(2),
TotalAmount = 99.99m
},
new CreateBookingRequest
{
GuestId = 2,
RoomId = 102,
CheckInDate = DateTime.Today.AddDays(5),
CheckOutDate = DateTime.Today.AddDays(7),
TotalAmount = 299.98m
}
};
foreach (var request in bookingRequests)
{
await _client.PostAsJsonAsync("/api/bookings", request);
}
// Act
var response = await _client.GetAsync("/api/bookings");
// Assert
response.Should().HaveStatusCode(HttpStatusCode.OK);
var bookings = await response.Content.ReadFromJsonAsync<List<Booking>>();
bookings.Should().NotBeNull();
bookings!.Should().HaveCountGreaterOrEqualTo(2);
}
}
// Database integration test with real database using TestContainers
public class BookingRepositoryIntegrationTests : IAsyncLifetime
{
private readonly SqlServerContainer _sqlContainer;
private HotelDbContext _context = null!;
public BookingRepositoryIntegrationTests()
{
_sqlContainer = new SqlServerBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithPassword("YourStrong!Passw0rd")
.Build();
}
public async Task InitializeAsync()
{
await _sqlContainer.StartAsync();
var connectionString = _sqlContainer.GetConnectionString();
var options = new DbContextOptionsBuilder<HotelDbContext>()
.UseSqlServer(connectionString)
.Options;
_context = new HotelDbContext(options);
await _context.Database.EnsureCreatedAsync();
// Seed test data
await SeedTestDataAsync();
}
public async Task DisposeAsync()
{
await _context.DisposeAsync();
await _sqlContainer.DisposeAsync();
}
private async Task SeedTestDataAsync()
{
_context.Guests.Add(new Guest { FirstName = "Test", LastName = "User", Email = "test@example.com" });
_context.Rooms.Add(new Room { RoomNumber = "201", RoomType = "Suite", PricePerNight = 199.99m, IsAvailable = true });
await _context.SaveChangesAsync();
}
[Fact]
public async Task AddBooking_WithValidData_SavesToDatabase()
{
// Arrange
var repository = new BookingRepository(_context);
var booking = new Booking
{
GuestId = 1,
RoomId = 1,
CheckInDate = DateTime.Today.AddDays(1),
CheckOutDate = DateTime.Today.AddDays(3),
TotalAmount = 399.98m
};
// Act
var result = await repository.AddAsync(booking);
// Assert
result.Should().NotBeNull();
result.Id.Should().BeGreaterThan(0);
// Verify in database
var savedBooking = await _context.Bookings.FindAsync(result.Id);
savedBooking.Should().NotBeNull();
savedBooking!.TotalAmount.Should().Be(399.98m);
}
}
When to Use and When Not to Use
Use Integration Testing when:
- Testing API endpoints and controllers
- Verifying database operations and queries
- Testing authentication and authorization
- Validating request/response serialization
- Testing middleware pipeline
- Verifying cross-component interactions
Don't use Integration Testing when:
- Testing pure business logic (use unit tests)
- Simple validation logic
- Performance-critical test suites
- Testing external service integrations (use contract tests)
Pros and Cons and Alternatives
Pros:
- Tests realistic scenarios
- Catches integration issues
- Validates entire request pipeline
- Tests actual database interactions
- Provides confidence in deployments
- Tests serialization/deserialization
Cons:
- Slower execution than unit tests
- More complex setup and teardown
- Harder to isolate failures
- Database state management complexity
- Resource intensive
- Can be flaky due to external dependencies
Alternatives:
- Contract testing (Pact)
- Component testing
- End-to-end testing
- Database unit tests
- API testing tools (Postman, Newman)