如何在Web Application Factory中替换Cosmos数据库用于集成测试?
解决Cosmos DB生产与测试环境数据库获取逻辑分离的方案
核心思路
抽象数据库获取逻辑,通过依赖注入在生产和测试环境提供不同实现:
- 生产环境:直接调用
GetDatabase,严格遵循最佳实践,不自动创建数据库 - 测试环境:创建唯一临时数据库(用
CreateDatabaseIfNotExists),测试完成后自动销毁
具体实现步骤
1. 定义抽象接口
创建IDatabaseProvider接口,封装数据库获取和清理逻辑:
public interface IDatabaseProvider { Task<Database> GetDatabaseAsync(CancellationToken cancellationToken = default); Task CleanupDatabaseAsync(CancellationToken cancellationToken = default); }
2. 生产环境实现
实现生产用的数据库提供者,仅获取已存在的数据库:
public class ProductionDatabaseProvider : IDatabaseProvider { private readonly CosmosClient _cosmosClient; private readonly string _databaseName; public ProductionDatabaseProvider(CosmosClient cosmosClient, IConfiguration configuration) { _cosmosClient = cosmosClient; _databaseName = configuration["Cosmos:DatabaseName"] ?? "Cocktails"; } public Task<Database> GetDatabaseAsync(CancellationToken cancellationToken = default) { // 生产环境直接获取,不执行创建操作 return Task.FromResult(_cosmosClient.GetDatabase(_databaseName)); } public Task CleanupDatabaseAsync(CancellationToken cancellationToken = default) { // 生产环境无需清理 return Task.CompletedTask; } }
3. 测试环境实现
创建测试用的提供者,生成唯一临时数据库,测试后销毁:
public class TestDatabaseProvider : IDatabaseProvider { private readonly CosmosClient _cosmosClient; private readonly string _testDatabaseName; private Database? _database; public TestDatabaseProvider(CosmosClient cosmosClient) { _cosmosClient = cosmosClient; // 生成唯一数据库名,避免并行测试冲突 _testDatabaseName = $"Cocktails_Test_{Guid.NewGuid():N}"; } public async Task<Database> GetDatabaseAsync(CancellationToken cancellationToken = default) { if (_database == null) { // 测试环境自动创建数据库(如果不存在) _database = await _cosmosClient.CreateDatabaseIfNotExistsAsync( id: _testDatabaseName, cancellationToken: cancellationToken); } return _database; } public async Task CleanupDatabaseAsync(CancellationToken cancellationToken = default) { if (_database != null) { // 测试完成后删除临时数据库 await _database.DeleteAsync(cancellationToken: cancellationToken); } } }
4. 注册服务
在生产代码中注册CosmosClient和生产用的数据库提供者:
// 注册CosmosClient单例(保留你原有的代码) builder.Services.AddSingleton<CosmosClient>(serviceProvider => { var client = new CosmosClient( accountEndpoint: Environment.GetEnvironmentVariable("Cosmos:Uri"), tokenCredential: new DefaultAzureCredential() ); return client; }); // 注册生产环境数据库提供者 builder.Services.AddSingleton<IDatabaseProvider, ProductionDatabaseProvider>();
5. 业务代码中使用
在服务类中注入IDatabaseProvider,替代直接调用CosmosClient获取数据库:
public class CocktailService { private readonly IDatabaseProvider _databaseProvider; public CocktailService(IDatabaseProvider databaseProvider) { _databaseProvider = databaseProvider; } public async Task CreateCocktailAsync(CocktailModel cocktail, CancellationToken cancellationToken) { var database = await _databaseProvider.GetDatabaseAsync(cancellationToken); var container = database.GetContainer("Recipes"); await container.CreateItemAsync(cocktail, cancellationToken: cancellationToken); } }
6. 测试中替换服务并清理
使用WebApplicationFactory替换服务,并通过IAsyncLifetime在测试前后处理数据库清理:
public class CocktailApiTests : IAsyncLifetime { private readonly WebApplicationFactory<Program> _factory; private readonly IDatabaseProvider _testDatabaseProvider; public CocktailApiTests() { _factory = new WebApplicationFactory<Program>() .WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // 移除生产环境的数据库提供者 var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IDatabaseProvider)); if (descriptor != null) { services.Remove(descriptor); } // 注册测试用的数据库提供者 services.AddSingleton<IDatabaseProvider, TestDatabaseProvider>(); }); }); // 获取测试数据库提供者实例,用于后续清理 _testDatabaseProvider = _factory.Services.GetRequiredService<IDatabaseProvider>(); } [Fact] public async Task CreateCocktail_ReturnsSuccessStatusCode() { var client = _factory.CreateClient(); var testCocktail = new CocktailResponse { Name = "Test Margarita", Instructions = "Mix tequila and lime" }; var response = await client.PostAsJsonAsync("/cocktails", testCocktail); response.EnsureSuccessStatusCode(); // 可添加更多断言,比如验证数据是否存入数据库 } // 测试完成后清理临时数据库 public async Task DisposeAsync() { await _testDatabaseProvider.CleanupDatabaseAsync(); await _factory.DisposeAsync(); } public Task InitializeAsync() => Task.CompletedTask; }
方案优势
- 符合最佳实践:生产环境严格使用
GetDatabase,避免意外创建数据库;测试环境用临时库,隔离测试数据 - 低侵入性:业务代码无需修改,仅通过依赖注入切换实现
- 并行测试安全:每个测试类生成唯一数据库名,避免测试间数据干扰
- 自动清理:测试完成后自动销毁临时数据库,不会留下冗余数据
内容的提问来源于stack exchange,提问作者Jakob Busk Sørensen
相关产品推荐
相关产品推荐

