Originally published on Medium · March 30, 2023 — examples target the tooling of that time.

Integration tests catch the bugs unit tests can't see — the ones that only show up when your code actually talks to a database or an external service. Here's how I structure integration tests in C#, with working examples for both scenarios.

So what actually separates an integration test from a unit test?

Integration tests play a critical role in evaluating the proper functioning of an application's supporting infrastructure, including the database, file system, and external dependencies. In contrast to unit tests that focus on individual code units in isolation, integration tests simulate real-world external dependencies to verify that components work together seamlessly, and to detect any defects that may arise during integration. The diagram below illustrates the boundaries of integration and unit tests:

Diagram illustrating the boundaries between integration tests and unit tests in an application

1. Testing database integration

When testing code that interacts with a database, it's important to test the integration between the code and the database. To do this, we'll need to create a test database that our integration tests can use. We'll also need to use an ORM (Object-Relational Mapping) tool like Entity Framework to interact with the database from our code.

Let's start by creating a simple C# class that retrieves data from a database using Entity Framework.

Setting up the application

Program.cs

using Microsoft.EntityFrameworkCore;
using Payment.Database;
using Payment.Repositories;
using Payment.Services;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var dbConnectionString = builder.Configuration.GetConnectionString("DbConnection");
builder.Services.AddDbContext<DatabaseContext>(options =>
    options.UseMySql(dbConnectionString, 
        new MariaDbServerVersion(ServerVersion.AutoDetect(dbConnectionString)))
        .UseLazyLoadingProxies());

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IPaymentRepository, PaymentRepository>();
builder.Services.AddHttpClient("order-client");

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Repository setup:

using Microsoft.EntityFrameworkCore;
using Payment.Database;
using Payment.Models.Entities;

namespace Payment.Repositories;

public interface IPaymentRepository
{
    Task<List<PaymentEntity>> GetAllPayment();
}

public class PaymentRepository : IPaymentRepository
{
    private readonly DatabaseContext _databaseContext;

    public PaymentRepository(DatabaseContext databaseContext)
    {
        _databaseContext = databaseContext;
    }

    public async Task<List<PaymentEntity>> GetAllPayment()
    {
        return await _databaseContext.Payments.ToListAsync();
    }
}

This PaymentRepository class retrieves all payments from a database using Entity Framework's ToListAsync() method. To test this code, we'll need to create an integration test that sets up a test database and verifies that the code correctly retrieves data from the database.

Here's an example integration test for this PaymentRepository class:

[Fact]
public async Task GetAll_ReturnsAllPayments()
{
    // Arrange
    var connectionString = "Server=localhost; Database=payments;User=root;Password=root";
    var databaseContextOptions = new DbContextOptionsBuilder<DatabaseContext>()
        .UseMySql(connectionString, 
            new MariaDbServerVersion(ServerVersion.AutoDetect(connectionString)));
    var databaseContext = new DatabaseContext(databaseContextOptions.Options);
    var paymentRepository = new PaymentRepository(databaseContext);
    var expectedUsers = await databaseContext.Payments.ToListAsync();

    // Act
    var actualUsers = await paymentRepository.GetAllPayment();

    // Assert
    Assert.Equal(expectedUsers.Count, actualUsers.Count);
    Assert.Equal(expectedUsers.First().Id, actualUsers.First().Id);
    Assert.Equal(expectedUsers.Last().Id, actualUsers.Last().Id);
}

2. Testing external HTTP service integration

First, we need to create a service that makes an HTTP call. In this example, let's create an OrderService that fetches an order from an external service using the HTTP protocol.

using Payment.Models;

namespace Payment.Services;

public interface IOrderService
{
    Task<Order?> GetOrder(CancellationToken ct);
}

public class OrderService : IOrderService
{
    private readonly HttpClient _httpClient;

    public OrderService(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient("order-client");
    }

    public async Task<Order?> GetOrder(CancellationToken ct)
    {
        var response = await _httpClient.GetAsync("http://localhost:5088/order", ct);
        return await response.Content.ReadFromJsonAsync<Order?>(cancellationToken:ct);
    }
}

Now that we have a service that makes an HTTP call, we can create an integration test that verifies that the service works correctly.

using Moq;
using Payment.Models;
using Payment.Services;
using Xunit;

namespace IntegrationTests;

public class OrderServiceIntegrationTests
{
    private readonly IOrderService _orderService;

    public OrderServiceIntegrationTests()
    {
        // Create IHttpClientFactory mock
        var httpClientFactoryMock = new Mock<IHttpClientFactory>();

        // Create a new HttpClient instance
        var httpClient = new HttpClient();

        // Set up the mock to return the new HttpClient instance when called with "order-client"
        httpClientFactoryMock.Setup(x => x.CreateClient("order-client")).Returns(httpClient);

        // Create an instance of OrderService
        _orderService = new OrderService(httpClientFactoryMock.Object);
    }

    [Fact]
    public async Task GetOrder_ShouldMakeHttpCall()
    {
        // Arrange
        var expectedOrder = new Order
        {
            Id = 1,
            Amount = 100
        };
        var cancellationToken = CancellationToken.None;

        // Act
        var result = await _orderService.GetOrder(cancellationToken);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedOrder.Id, result.Id);
        Assert.Equal(expectedOrder.Amount, result.Amount);
    }
}

In conclusion, integration tests in C# are crucial to ensure a software system functions as expected by testing interactions between external components. By following the examples provided, developers can create effective integration tests while improving the overall quality and stability of the software.

Thank you for reading!