Skip to content

MediatR 单元测试实战 ​

📋 概述 ​

MediatR 的设计使得代码非常容易进行单元测试。本文将深入探讨如何测试 MediatR 相关的各种组件,包括命令处理器、查询处理器、管道行为和领域事件处理器。

学习目标 ​

  • ✅ 掌握命令/查询处理器的单元测试
  • ✅ 学习 Mock 依赖服务的技巧
  • ✅ 测试管道行为(Pipeline Behaviors)
  • ✅ 测试领域事件处理器
  • ✅ 实践集成测试策略
  • ✅ 使用 FluentAssertions 提高断言可读性

🧪 测试框架选择 ​

推荐技术栈 ​

工具用途版本
xUnit测试框架2.6+
MoqMock 框架4.20+
FluentAssertions断言库6.12+
Bogus测试数据生成35.0+
AutoFixture测试数据自动化4.18+

安装 NuGet 包 ​

bash
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package Bogus
dotnet add package AutoFixture

🎯 基础测试 ​

1. 简单命令处理器测试 ​

被测代码 ​

csharp
// Application/Commands/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _orderRepository;
    private readonly ICustomerRepository _customerRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _customerRepository = customerRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        // 验证客户存在
        var customer = await _customerRepository.GetByIdAsync(request.CustomerId, cancellationToken);
        if (customer == null)
            throw new ApplicationException($"客户 {request.CustomerId} 不存在");

        // 创建订单项
        var orderItems = request.Items.Select(item => 
            new OrderItem(item.ProductId, item.ProductName, item.Quantity, Money.FromDecimal(item.UnitPrice))
        ).ToList();

        // 创建订单
        var order = new Order(request.CustomerId, request.ShippingAddress.ToDomain(), orderItems);

        // 持久化
        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}

单元测试 ​

csharp
// Tests/OrderManagement.UnitTests/Commands/CreateOrderTests.cs
using FluentAssertions;
using Moq;
using OrderManagement.Application.Commands.Orders.CreateOrder;
using OrderManagement.Application.Interfaces;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
using Xunit;

namespace OrderManagement.UnitTests.Commands;

public class CreateOrderTests
{
    private readonly Mock<IOrderRepository> _orderRepositoryMock;
    private readonly Mock<ICustomerRepository> _customerRepositoryMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly CreateOrderHandler _handler;

    public CreateOrderTests()
    {
        _orderRepositoryMock = new Mock<IOrderRepository>();
        _customerRepositoryMock = new Mock<ICustomerRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        
        _handler = new CreateOrderHandler(
            _orderRepositoryMock.Object,
            _customerRepositoryMock.Object,
            _unitOfWorkMock.Object);
    }

    [Fact]
    public async Task Handle_ValidCommand_ShouldCreateOrderAndReturnId()
    {
        // Arrange
        var customer = new Customer("张三", "13800138000");
        _customerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(customer);

        var command = new CreateOrderCommand
        {
            CustomerId = 1,
            ShippingAddress = new AddressDto
            {
                Province = "广东省",
                City = "深圳市",
                District = "南山区",
                Street = "科技园南路",
                ZipCode = "518000"
            },
            Items = new List<OrderItemDto>
            {
                new() { ProductId = 1, ProductName = "iPhone 15", Quantity = 1, UnitPrice = 7999 }
            }
        };

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().BeGreaterThan(0);
        _orderRepositoryMock.Verify(r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once);
        _unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_InvalidCustomerId_ShouldThrowException()
    {
        // Arrange
        _customerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync((Customer)null);

        var command = new CreateOrderCommand
        {
            CustomerId = 999,
            ShippingAddress = new AddressDto { /* ... */ },
            Items = new List<OrderItemDto> { /* ... */ }
        };

        // Act
        Func<Task> act = async () => await _handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<ApplicationException>()
            .WithMessage("*客户*不存在");
    }

    [Fact]
    public async Task Handle_EmptyItems_ShouldThrowException()
    {
        // Arrange
        var customer = new Customer("张三", "13800138000");
        _customerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(customer);

        var command = new CreateOrderCommand
        {
            CustomerId = 1,
            ShippingAddress = new AddressDto { /* ... */ },
            Items = new List<OrderItemDto>() // 空列表
        };

        // Act
        Func<Task> act = async () => await _handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<ArgumentException>()
            .WithMessage("*商品不能为空*");
    }
}

2. 查询处理器测试 ​

被测代码 ​

csharp
// Application/Queries/Orders/GetOrder/GetOrderHandler.cs
public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto?>
{
    private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

    public GetOrderHandler(IDbContextFactory<ApplicationDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task<OrderDto?> Handle(GetOrderQuery request, CancellationToken cancellationToken)
    {
        await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
        
        var order = await context.Orders
            .Include(o => o.OrderItems)
            .Include(o => o.Customer)
            .AsNoTracking()
            .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);

        if (order == null)
            return null;

        return new OrderDto
        {
            Id = order.Id,
            OrderNumber = order.OrderNumber,
            CustomerId = order.CustomerId,
            CustomerName = order.Customer.Name,
            Status = order.Status.ToString(),
            Items = order.OrderItems.Select(item => new OrderItemDto
            {
                ProductId = item.ProductId,
                ProductName = item.ProductName,
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice.Amount,
                TotalPrice = item.TotalPrice.Amount
            }).ToList(),
            TotalAmount = order.TotalAmount.Amount,
            Currency = order.TotalAmount.Currency,
            CreatedAt = order.CreatedAt,
            UpdatedAt = order.UpdatedAt
        };
    }
}

单元测试 ​

csharp
// Tests/OrderManagement.UnitTests/Queries/GetOrderTests.cs
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using OrderManagement.Application.Queries.Orders.GetOrder;
using OrderManagement.Infrastructure.Persistence;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
using OrderManagement.Domain.ValueObjects;
using Xunit;

namespace OrderManagement.UnitTests.Queries;

public class GetOrderTests : IDisposable
{
    private readonly ApplicationDbContext _context;
    private readonly DbContextFactory<ApplicationDbContext> _contextFactory;
    private readonly GetOrderHandler _handler;

    public GetOrderTests()
    {
        // 使用内存数据库
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        _context = new ApplicationDbContext(options);
        _contextFactory = new DbContextFactory<ApplicationDbContext>(_context);
        _handler = new GetOrderHandler(_contextFactory);
    }

    [Fact]
    public async Task Handle_ExistingOrder_ShouldReturnOrderDto()
    {
        // Arrange
        var order = CreateTestOrder();
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        var query = new GetOrderQuery(order.Id);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(order.Id);
        result.OrderNumber.Should().Be(order.OrderNumber);
        result.CustomerName.Should().Be(order.Customer.Name);
        result.Items.Should().HaveCount(order.OrderItems.Count);
        result.TotalAmount.Should().Be(order.TotalAmount.Amount);
    }

    [Fact]
    public async Task Handle_NonExistentOrder_ShouldReturnNull()
    {
        // Arrange
        var query = new GetOrderQuery(999);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task Handle_ShouldUseAsNoTracking_ForReadOperation()
    {
        // Arrange
        var order = CreateTestOrder();
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        var query = new GetOrderQuery(order.Id);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert - 验证是只读查询(不会跟踪实体)
        var trackedEntities = _context.ChangeTracker.Entries().Count();
        trackedEntities.Should().Be(0);
    }

    private Order CreateTestOrder()
    {
        var customer = new Customer("张三", "13800138000");
        _context.Customers.Add(customer);

        var address = new Address("广东省", "深圳市", "南山区", "科技园南路", "518000");
        var items = new List<OrderItem>
        {
            new OrderItem(1, "iPhone 15", 1, Money.FromDecimal(7999)),
            new OrderItem(2, "AirPods Pro", 2, Money.FromDecimal(1899))
        };

        return new Order(customer.Id, address, items);
    }

    public void Dispose()
    {
        _context.Database.EnsureDeleted();
        _context.Dispose();
    }
}

🔧 管道行为测试 ​

1. 验证行为测试 ​

被测代码 ​

csharp
// Application/Behaviors/ValidationBehavior.cs
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
        
        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

单元测试 ​

csharp
// Tests/OrderManagement.UnitTests/Behaviors/ValidationBehaviorTests.cs
using FluentAssertions;
using FluentValidation;
using Moq;
using OrderManagement.Application.Behaviors;
using OrderManagement.Application.Commands.Orders.CreateOrder;
using Xunit;

namespace OrderManagement.UnitTests.Behaviors;

public class ValidationBehaviorTests
{
    private readonly Mock<IValidator<CreateOrderCommand>> _validatorMock;
    private readonly ValidationBehavior<CreateOrderCommand, int> _behavior;

    public ValidationBehaviorTests()
    {
        _validatorMock = new Mock<IValidator<CreateOrderCommand>>();
        _behavior = new ValidationBehavior<CreateOrderCommand, int>(
            new[] { _validatorMock.Object });
    }

    [Fact]
    public async Task Handle_ValidRequest_ShouldCallNext()
    {
        // Arrange
        var request = new CreateOrderCommand { /* 有效数据 */ };
        var nextCalled = false;

        Task<int> Next()
        {
            nextCalled = true;
            return Task.FromResult(1);
        }

        _validatorMock.Setup(v => v.ValidateAsync(It.IsAny<ValidationContext<CreateOrderCommand>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValidationResult()); // 验证通过

        // Act
        var result = await _behavior.Handle(request, Next, CancellationToken.None);

        // Assert
        result.Should().Be(1);
        nextCalled.Should().BeTrue();
    }

    [Fact]
    public async Task Handle_InvalidRequest_ShouldThrowValidationException()
    {
        // Arrange
        var request = new CreateOrderCommand { /* 无效数据 */ };

        var validationResult = new ValidationResult(new[]
        {
            new ValidationFailure("CustomerId", "客户ID无效"),
            new ValidationFailure("Items", "订单商品不能为空")
        });

        _validatorMock.Setup(v => v.ValidateAsync(It.IsAny<ValidationContext<CreateOrderCommand>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(validationResult);

        Task<int> Next() => Task.FromResult(1);

        // Act
        Func<Task> act = async () => await _behavior.Handle(request, Next, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<ValidationException>()
            .Where(ex => ex.Errors.Count == 2)
            .Where(ex => ex.Errors.Any(e => e.PropertyName == "CustomerId"))
            .Where(ex => ex.Errors.Any(e => e.PropertyName == "Items"));
    }

    [Fact]
    public async Task Handle_NoValidators_ShouldCallNext()
    {
        // Arrange
        var behaviorWithoutValidators = new ValidationBehavior<CreateOrderCommand, int>(
            Enumerable.Empty<IValidator<CreateOrderCommand>>());

        var request = new CreateOrderCommand();
        var nextCalled = false;

        Task<int> Next()
        {
            nextCalled = true;
            return Task.FromResult(1);
        }

        // Act
        var result = await behaviorWithoutValidators.Handle(request, Next, CancellationToken.None);

        // Assert
        result.Should().Be(1);
        nextCalled.Should().BeTrue();
    }
}

2. 日志行为测试 ​

csharp
// Application/Behaviors/LoggingBehavior.cs
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation("开始处理请求: {RequestName}", requestName);

        var stopwatch = Stopwatch.StartNew();
        try
        {
            var response = await next();
            stopwatch.Stop();

            _logger.LogInformation(
                "请求处理完成: {RequestName}, 耗时: {ElapsedMs}ms", 
                requestName, 
                stopwatch.ElapsedMilliseconds);

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex, 
                "请求处理失败: {RequestName}, 耗时: {ElapsedMs}ms", 
                requestName, 
                stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}

// Tests
public class LoggingBehaviorTests
{
    private readonly Mock<ILogger<LoggingBehavior<CreateOrderCommand, int>>> _loggerMock;
    private readonly LoggingBehavior<CreateOrderCommand, int> _behavior;

    public LoggingBehaviorTests()
    {
        _loggerMock = new Mock<ILogger<LoggingBehavior<CreateOrderCommand, int>>>();
        _behavior = new LoggingBehavior<CreateOrderCommand, int>(_loggerMock.Object);
    }

    [Fact]
    public async Task Handle_SuccessfulRequest_ShouldLogStartAndCompletion()
    {
        // Arrange
        var request = new CreateOrderCommand();
        var logs = new List<string>();

        Task<int> Next() => Task.FromResult(1);

        // Act
        var result = await _behavior.Handle(request, Next, CancellationToken.None);

        // Assert
        result.Should().Be(1);
        
        // 验证日志被调用(简化示例,实际应验证日志内容)
        _loggerMock.Verify(
            x => x.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("开始处理请求")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()),
            Times.AtLeastOnce);
    }

    [Fact]
    public async Task Handle_FailedRequest_ShouldLogError()
    {
        // Arrange
        var request = new CreateOrderCommand();

        Task<int> Next() => throw new ApplicationException("测试异常");

        // Act
        Func<Task> act = async () => await _behavior.Handle(request, Next, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<ApplicationException>();

        _loggerMock.Verify(
            x => x.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("请求处理失败")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()),
            Times.Once);
    }
}

📡 事件处理器测试 ​

1. 单个事件处理器测试 ​

被测代码 ​

csharp
// Application/EventHandlers/OrderCreatedEmailHandler.cs
public class OrderCreatedEmailHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderCreatedEmailHandler> _logger;

    public OrderCreatedEmailHandler(
        IEmailService emailService,
        ILogger<OrderCreatedEmailHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation("发送订单确认邮件,订单ID: {OrderId}", notification.OrderId);

        var emailMessage = new EmailMessage
        {
            To = await GetCustomerEmail(notification.CustomerId),
            Subject = $"订单确认 - {notification.OrderNumber}",
            Body = GenerateOrderConfirmationEmail(notification)
        };

        await _emailService.SendAsync(emailMessage, cancellationToken);
    }

    private async Task<string> GetCustomerEmail(int customerId) { /* ... */ }
    private string GenerateOrderConfirmationEmail(OrderCreatedEvent orderEvent) { /* ... */ }
}

单元测试 ​

csharp
// Tests/OrderManagement.UnitTests/EventHandlers/OrderCreatedEmailHandlerTests.cs
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using OrderManagement.Application.EventHandlers;
using OrderManagement.Application.Interfaces;
using OrderManagement.Domain.Events;
using Xunit;

namespace OrderManagement.UnitTests.EventHandlers;

public class OrderCreatedEmailHandlerTests
{
    private readonly Mock<IEmailService> _emailServiceMock;
    private readonly Mock<ILogger<OrderCreatedEmailHandler>> _loggerMock;
    private readonly OrderCreatedEmailHandler _handler;

    public OrderCreatedEmailHandlerTests()
    {
        _emailServiceMock = new Mock<IEmailService>();
        _loggerMock = new Mock<ILogger<OrderCreatedEmailHandler>>();
        _handler = new OrderCreatedEmailHandler(
            _emailServiceMock.Object,
            _loggerMock.Object);
    }

    [Fact]
    public async Task Handle_ShouldSendEmail_WhenOrderCreated()
    {
        // Arrange
        var orderEvent = new OrderCreatedEvent
        {
            OrderId = 1,
            OrderNumber = Guid.NewGuid(),
            CustomerId = 1,
            TotalAmount = 7999,
            Items = new List<OrderItemSnapshot>
            {
                new() { ProductId = 1, ProductName = "iPhone 15", Quantity = 1, UnitPrice = 7999 }
            },
            CreatedAt = DateTime.UtcNow
        };

        // Act
        await _handler.Handle(orderEvent, CancellationToken.None);

        // Assert
        _emailServiceMock.Verify(
            x => x.SendAsync(
                It.Is<EmailMessage>(m => 
                    m.Subject.Contains(orderEvent.OrderNumber.ToString()) &&
                    m.Body.Contains("订单确认")),
                It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task Handle_EmailServiceThrows_ShouldNotPropagateException()
    {
        // Arrange
        var orderEvent = new OrderCreatedEvent { /* ... */ };

        _emailServiceMock.Setup(x => x.SendAsync(It.IsAny<EmailMessage>(), It.IsAny<CancellationToken>()))
            .ThrowsAsync(new ApplicationException("邮件服务故障"));

        // Act & Assert
        // 不应该抛出异常,避免影响主流程
        await _handler.Handle(orderEvent, CancellationToken.None);
        
        // 验证没有异常传播
        true.Should().BeTrue();
    }
}

2. 多个事件处理器测试 ​

csharp
// Tests/OrderManagement.IntegrationTests/Events/OrderCreatedEventTests.cs
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using OrderManagement.Application.EventHandlers;
using OrderManagement.Domain.Events;
using OrderManagement.Infrastructure.Persistence;
using Xunit;

namespace OrderManagement.IntegrationTests.Events;

public class OrderCreatedEventTests : IClassFixture<IntegrationTestFixture>
{
    private readonly IntegrationTestFixture _fixture;

    public OrderCreatedEventTests(IntegrationTestFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task MultipleHandlers_AllShouldBeExecuted()
    {
        // Arrange
        var mediator = _fixture.Services.GetRequiredService<IMediator>();
        var orderEvent = new OrderCreatedEvent
        {
            OrderId = 1,
            OrderNumber = Guid.NewGuid(),
            CustomerId = 1,
            TotalAmount = 7999,
            Items = new List<OrderItemSnapshot>(),
            CreatedAt = DateTime.UtcNow
        };

        // Act
        await mediator.Publish(orderEvent);

        // Assert
        // 验证所有处理器都被执行
        // 可以通过检查数据库、日志或外部系统来验证
        var emailSent = await CheckEmailSentAsync(orderEvent.OrderId);
        var inventoryReserved = await CheckInventoryReservedAsync(orderEvent.OrderId);
        var analyticsTracked = await CheckAnalyticsTrackedAsync(orderEvent.OrderId);

        emailSent.Should().BeTrue("邮件应该已发送");
        inventoryReserved.Should().BeTrue("库存应该已预留");
        analyticsTracked.Should().BeTrue("分析数据应该已记录");
    }
}

🏗️ 集成测试 ​

1. 测试基础设施 ​

csharp
// Tests/OrderManagement.IntegrationTests/IntegrationTestFixture.cs
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using OrderManagement.Infrastructure.Persistence;
using Testcontainers.PostgreSql;

namespace OrderManagement.IntegrationTests;

public class IntegrationTestFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _dbContainer;
    private WebApplicationFactory<Program> _factory;
    
    public IServiceProvider Services => _factory.Services;

    public IntegrationTestFixture()
    {
        _dbContainer = new PostgreSqlBuilder()
            .WithImage("postgres:15-alpine")
            .WithDatabase("testdb")
            .WithUsername("test")
            .WithPassword("test")
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();

        _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(_dbContainer.GetConnectionString()));
                });
            });

        // 运行数据库迁移
        using var scope = Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        await context.Database.MigrateAsync();
    }

    public async Task DisposeAsync()
    {
        await _dbContainer.StopAsync();
        _factory.Dispose();
    }
}

2. API 集成测试 ​

csharp
// Tests/OrderManagement.IntegrationTests/Api/OrdersControllerTests.cs
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using OrderManagement.Application.Commands.Orders.CreateOrder;
using OrderManagement.Application.Queries.Orders.GetOrder;

namespace OrderManagement.IntegrationTests.Api;

public class OrdersControllerTests : IClassFixture<IntegrationTestFixture>
{
    private readonly IntegrationTestFixture _fixture;
    private readonly HttpClient _client;

    public OrdersControllerTests(IntegrationTestFixture fixture)
    {
        _fixture = fixture;
        _client = _fixture.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
    }

    [Fact]
    public async Task CreateOrder_ValidRequest_ShouldReturnCreated()
    {
        // Arrange
        var command = new CreateOrderCommand
        {
            CustomerId = 1,
            ShippingAddress = new AddressDto
            {
                Province = "广东省",
                City = "深圳市",
                District = "南山区",
                Street = "科技园南路",
                ZipCode = "518000"
            },
            Items = new List<OrderItemDto>
            {
                new() { ProductId = 1, ProductName = "iPhone 15", Quantity = 1, UnitPrice = 7999 }
            }
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/orders", command);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        
        var orderId = await response.Content.ReadFromJsonAsync<int>();
        orderId.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task GetOrder_ExistingOrder_ShouldReturnOrder()
    {
        // Arrange - 先创建订单
        var orderId = await CreateTestOrderAsync();

        // Act
        var response = await _client.GetAsync($"/api/orders/{orderId}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        
        var order = await response.Content.ReadFromJsonAsync<OrderDto>();
        order.Should().NotBeNull();
        order.Id.Should().Be(orderId);
    }

    [Fact]
    public async Task GetOrder_NonExistentOrder_ShouldReturnNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/orders/999");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    private async Task<int> CreateTestOrderAsync()
    {
        var command = new CreateOrderCommand { /* ... */ };
        var response = await _client.PostAsJsonAsync("/api/orders", command);
        return await response.Content.ReadFromJsonAsync<int>();
    }
}

🎨 高级测试技巧 ​

1. 使用 Bogus 生成测试数据 ​

csharp
// Tests/OrderManagement.UnitTests/TestDataGenerators/OrderFaker.cs
using Bogus;
using OrderManagement.Application.Commands.Orders.CreateOrder;

namespace OrderManagement.UnitTests.TestDataGenerators;

public class OrderFaker : Faker<CreateOrderCommand>
{
    public OrderFaker()
    {
        RuleFor(x => x.CustomerId, f => f.Random.Int(1, 1000));
        RuleFor(x => x.Remark, f => f.Lorem.Sentence());
        
        RuleFor(x => x.ShippingAddress, f => new AddressDto
        {
            Province = f.Address.State(),
            City = f.Address.City(),
            District = f.Address.County(),
            Street = f.Address.StreetAddress(),
            ZipCode = f.Address.ZipCode()
        });
        
        RuleFor(x => x.Items, f => new List<OrderItemDto>
        {
            new()
            {
                ProductId = f.Commerce.ProductId(),
                ProductName = f.Commerce.ProductName(),
                Quantity = f.Random.Int(1, 10),
                UnitPrice = decimal.Parse(f.Commerce.Price()),
                Currency = "CNY"
            }
        });
    }
}

// 使用示例
public class CreateOrderWithFakerTests
{
    [Fact]
    public async Task Handle_GeneratedData_ShouldWork()
    {
        // Arrange
        var faker = new OrderFaker();
        var command = faker.Generate();

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().BeGreaterThan(0);
    }
}

2. 理论测试(Theory Tests) ​

csharp
public class ValidationTheoryTests
{
    [Theory]
    [InlineData(-1, "客户ID必须大于0")]
    [InlineData(0, "客户ID必须大于0")]
    [InlineData(1, null)] // 有效
    public async Task Handle_CustomerIdValidation_ShouldBehaveCorrectly(
        int customerId, 
        string expectedError)
    {
        // Arrange
        var command = new CreateOrderCommand { CustomerId = customerId, /* ... */ };

        // Act
        Func<Task> act = async () => await _handler.Handle(command, CancellationToken.None);

        // Assert
        if (expectedError != null)
        {
            await act.Should().ThrowAsync<ValidationException>()
                .Where(ex => ex.Errors.Any(e => e.ErrorMessage.Contains(expectedError)));
        }
        else
        {
            await act.Should().NotThrowAsync();
        }
    }
}

3. 快照测试 ​

csharp
// Tests/OrderManagement.UnitTests/Snapshots/OrderDtoSnapshotTests.cs
using VerifyXunit;
using OrderManagement.Application.Queries.Orders.GetOrder;

[UsesVerify]
public class OrderDtoSnapshotTests
{
    [Fact]
    public Task GetOrder_Snapshot_ShouldMatch()
    {
        // Arrange
        var orderDto = new OrderDto
        {
            Id = 1,
            OrderNumber = Guid.Parse("00000000-0000-0000-0000-000000000001"),
            CustomerName = "张三",
            Status = "Pending",
            TotalAmount = 7999,
            Currency = "CNY",
            CreatedAt = new DateTime(2024, 1, 1, 12, 0, 0)
        };

        // Act & Assert
        return Verifier.Verify(orderDto);
    }
}

📊 测试覆盖率 ​

使用 Coverlet 收集覆盖率 ​

bash
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage.lcov

目标覆盖率 ​

组件类型目标覆盖率
命令处理器90%+
查询处理器85%+
管道行为95%+
事件处理器80%+
验证器90%+

🎯 最佳实践总结 ​

✅ 推荐做法 ​

  1. 隔离测试:每个测试独立,不共享状态
  2. 命名清晰:Method_Scenario_ExpectedBehavior
  3. AAA 模式:Arrange-Act-Assert
  4. Mock 外部依赖:数据库、API、文件系统
  5. 使用 FluentAssertions:提高可读性
  6. 测试边界条件:null、空集合、极值
  7. 异步测试:使用 async/await,避免 .Result

❌ 避免做法 ​

  1. 测试实现细节:关注行为而非实现
  2. 过度 Mock:不要 Mock 被测对象本身
  3. 脆弱的测试:避免依赖时间、随机数
  4. 测试太多内容:每个测试关注一个场景
  5. 忽略清理:确保 Dispose 资源

📚 参考资源 ​

Released under the CC BY-SA 4.0 License.