我正在集成测试一个 .NET API,该 API 使用存储在 app.settings.json 文件中的连接字符串和数据库名称值连接到 MongoDB。我正在使用 xUnit 和测试容器来执行测试。进行测试时,环境变量发生变化并提示我的 Program.cs 连接到测试数据库。我最初想在每次测试之间擦除并重新设定此测试数据库或执行某种回滚,但我找不到简单的方法来做到这一点。
我现在尝试创建测试数据库的本地模拟,但是当我在测试中构建并连接到 API 时,它仍然指向实时测试数据库而不是本地数据库。我希望能够将 app.settings.json 中的连接字符串更改为每次测试运行时在测试装置中生成的连接字符串,但我不确定如何执行此操作。我已经研究过创建自定义 WebFactory 和依赖项注入,但不确定如何使用迄今为止的代码来实现它。
我的 Program.cs 文件如下所示
public partial class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
bool isTestEnvironment = Helper.IsTestEnvironment();
builder.Services.Configure<ArtsieDatabaseSettings>(
builder.Configuration.GetSection(isTestEnvironment ? "ArtsieTestDatabase" : "ArtsieDatabase"));
builder.Services.AddSingleton<ArtsieService>();
builder.Services.AddControllers()
.AddJsonOptions(
options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Art API", Description = "Browse some beautiful art", Version = "v1" });
});
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
//... rest of swagger documentation and endpoints
app.Run();
}
}
我的测试文件如下所示:
namespace art_api.Tests;
using ArtsieApi.Models;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using System.Net;
using System.Text;
using MongoDB.Driver;
using Testcontainers.MongoDb;
using Xunit;
public class DatabaseFixture : IDisposable
{
private readonly IMongoDatabase _database;
private readonly MongoDbContainer _container;
public DatabaseFixture()
{
Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", "true");
_container = new MongoDbBuilder().Build();
_container.StartAsync().Wait();
var connectionString = _container.GetConnectionString();
var client = new MongoClient(connectionString);
_database = client.GetDatabase("artsie-test");
// Seed initial data
SeedTestData();
}
private void SeedTestData()
{
var artCollection = _database.GetCollection<List<Art>>("art");
var commentsCollection = _database.GetCollection<List<Comment>>("comments");
var usersCollection = _database.GetCollection<List<User>>("users");
}
public void Dispose()
{
_database.DropCollection("art");
_database.DropCollection("comments");
_database.DropCollection("users");
_container.DisposeAsync().GetAwaiter().GetResult();
Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", null);
}
}
public class Endpoints : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
public Endpoints(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact(DisplayName = "200: GET /")]
public async Task TestRootEndpoint()
{
await using var application = new WebApplicationFactory<Program>();
using var client = application.CreateClient();
var response = await client.GetAsync("/");
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World!", content);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
//... rest of tests
}
经过一番挫折后,在 Google/YouTube/ChatGPT 的帮助下,我找到了解决方案!
首先,我创建了一个 CustomWebApplicationFactory 类,该类获取 Fixture 类中生成的本地连接字符串并将其插入到 DatabaseSettings 的新实例中。
我还修复了 SeedTestData 函数,以使用存储在一些 .json 文件中的测试数据填充本地数据库。
我的测试现在在构建测试应用程序时使用 CustomWebApplicationFactory。很确定还有更多可以重构的地方,但我很高兴找到了解决方案!
internal class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly DatabaseFixture _databaseFixture;
public CustomWebApplicationFactory(DatabaseFixture databaseFixture)
{
_databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture));
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IOptions<ArtsieDatabaseSettings>));
// Access the MongoDbContainer from DatabaseFixture
var container = _databaseFixture._container;
// Create a new instance of ArtsieDatabaseSettings with test connection string
var testConnectionString = container.GetConnectionString();
var testArtsieSettings = new ArtsieDatabaseSettings
{
ConnectionString = testConnectionString,
DatabaseName = "artsie-test",
ArtCollectionName = "art",
CommentsCollectionName = "comments",
UsersCollectionName = "users"
};
// Register the new instance of IOptions<ArtsieDatabaseSettings>
services.AddSingleton<IOptions<ArtsieDatabaseSettings>>(_ => Options.Create(testArtsieSettings));
});
}
}
public class DatabaseFixture : IDisposable
{
private readonly IMongoDatabase _database;
public MongoDbContainer _container { get; private set; }
public DatabaseFixture()
{
Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", "true");
// Initialize Testcontainers.MongoDb MongoDB container
_container = new MongoDbBuilder().Build();
// Start the container
_container.StartAsync().Wait();
// Get MongoDB connection string
var connectionString = _container.GetConnectionString();
// Connect to the MongoDB database
var client = new MongoClient(connectionString);
_database = client.GetDatabase("artsie-test");
// Seed initial data
SeedTestData();
}
private void SeedTestData()
{
string currentDirectory = AppDomain.CurrentDomain.BaseDirectory;
string dataFolderPath = Path.Combine(currentDirectory, "data");
string artFilePath = Path.Combine(dataFolderPath, "art.json");
string commentsFilePath = Path.Combine(dataFolderPath, "comments.json");
string usersFilePath = Path.Combine(dataFolderPath, "users.json");
// Read JSON file containing test data
string artJson = File.ReadAllText(artFilePath);
string commentsJson = File.ReadAllText(commentsFilePath);
string usersJson = File.ReadAllText(usersFilePath);
// Deserialize JSON into list of objects
var artData = JsonConvert.DeserializeObject<List<Art>>(artJson);
var commentsData = JsonConvert.DeserializeObject<List<Comment>>(commentsJson);
var usersData = JsonConvert.DeserializeObject<List<User>>(usersJson);
// Implement seeding logic to populate the database with test data
// For example:
var artCollection = _database.GetCollection<Art>("art");
var commentsCollection = _database.GetCollection<Comment>("comments");
var usersCollection = _database.GetCollection<User>("users");
artCollection.InsertMany(artData);
commentsCollection.InsertMany(commentsData);
usersCollection.InsertMany(usersData);
}
public void Dispose()
{
// Clean up after tests
// Drop collections or perform any necessary cleanup operations
// For example:
_database.DropCollection("art");
_database.DropCollection("comments");
_database.DropCollection("users");
// Dispose of the container
_container.DisposeAsync().GetAwaiter().GetResult();
Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", null);
}
}
public class Endpoints : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
public Endpoints(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact(DisplayName = "200: GET /")]
public async Task TestRootEndpoint()
{
await using var application = new CustomWebApplicationFactory(_fixture);
using var client = application.CreateClient();
var response = await client.GetAsync("/");
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World!", content);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
如果您有
IOptionsMonitor
,最好:
services.Configure<ArtsieDatabaseSettings>(t =>
{
t.ConnectionString = testConnectionString;
t.DatabaseName = "artsie-test";
t.ArtCollectionName = "art";
t.CommentsCollectionName = "comments";
t.UsersCollectionName = "users";
});
就像解释的那样这里