MediatR 单元测试实战
📋 概述
MediatR 的设计使得代码非常容易进行单元测试。本文将深入探讨如何测试 MediatR 相关的各种组件,包括命令处理器、查询处理器、管道行为和领域事件处理器。
学习目标
- ✅ 掌握命令/查询处理器的单元测试
- ✅ 学习 Mock 依赖服务的技巧
- ✅ 测试管道行为(Pipeline Behaviors)
- ✅ 测试领域事件处理器
- ✅ 实践集成测试策略
- ✅ 使用 FluentAssertions 提高断言可读性
🧪 测试框架选择
推荐技术栈
| 工具 | 用途 | 版本 |
|---|---|---|
| xUnit | 测试框架 | 2.6+ |
| Moq | Mock 框架 | 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%+ |
🎯 最佳实践总结
✅ 推荐做法
- 隔离测试:每个测试独立,不共享状态
- 命名清晰:
Method_Scenario_ExpectedBehavior - AAA 模式:Arrange-Act-Assert
- Mock 外部依赖:数据库、API、文件系统
- 使用 FluentAssertions:提高可读性
- 测试边界条件:null、空集合、极值
- 异步测试:使用
async/await,避免.Result
❌ 避免做法
- 测试实现细节:关注行为而非实现
- 过度 Mock:不要 Mock 被测对象本身
- 脆弱的测试:避免依赖时间、随机数
- 测试太多内容:每个测试关注一个场景
- 忽略清理:确保
Dispose资源