Skip to main content

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)