@Muflix 我无法在您的评论问题太长之后添加评论,所以这里是...
我正在使用 xUnit 和 Shouldly:
而不是在构造函数中创建作用域服务然后从测试中访问我一直在注入工厂(存储为private readonly CustomWebApplicationFactory<Startup> _factory;)然后在测试中我使用工厂创建一个作用域并访问一个作用域来自每个测试的服务(例如dbcontext)。由于WebApplicationFactory 是 class fixture 或 collection fixture,测试共享数据库,这通过 single 数据库连接它使用了 DatabaseFixture 成员(请注意,这并不是严格地用作 fixture 在这里它只是简单地实例化为 Web 应用程序工厂的成员,它被称为因为它在我的代码中的其他地方用作单元测试夹具)。 WebApplicationFactory 中的构造函数正确实例化了 DatabaseFixture 类,该类反过来打开数据库连接,并且仅在处置 WebApplicationFactory(以及因此 DatabaseFixture)时才关闭它。
我的测试:
public class MyControllerTests : IClassFixture<MySQLAppFactory<Startup>> // see below for MySQLAppFactory
{
private readonly MySQLAppFactory<Startup> _factory; // shared for ALL tests in this class (classFixture)
public MyControllerTests(MySQLAppFactory<Startup> factory)
{
_factory = factory;
}
[Theory] //example of many tests all of which use SAME _factory instance
[JsonArrayData("Path-to-Somewhere/MyData.json")] // pass some data
public async Task Post_should_do_whatever(MyRequest request) // I'm using Shouldly
{
var client = _factory.CreateClient(); // create a client form the single instance of webApplicationFactory
var httpContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response = await client.PostAsync($"api/my", httpContent);
response.StatusCode.ShouldBe(HttpStatusCode.Whatever);
}
}
MySQLAppFactory 在构造期间创建自己的单个 DatabaseFixture (TestDatabase)(它从服务中删除现有的数据库上下文并替换为使用此 DatabaseFixture 的上下文)。这个相同的数据库类持续存在(连接保持打开),并且在整个集成测试中使用。每次创建新上下文时,都会附加到同一个数据库连接。它与用于单元测试的基类相同(但在单元测试的情况下,我主要使用 SQLite 派生数据库,而不是 MySQL)。
public class MySQLAppFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
private readonly DatabaseFixture _databaseFixture; // THIS factory has THE single Database Fixture instance here
private bool _disposed;
public MySQLAppFactory()
{
_databaseFixture = new MySQLFixture(); // Create the single instance of MySQL fixture here
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseEnvironment("Testing")
.ConfigureServices(services => //also (as of ASP NET Core 3.0) runs after TStartup.ConfigureServices
{
// remove DbcontextOptions from original API project
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<MyContext>)); services.Remove(descriptor);
// add the test database context instead
services.AddScoped<MyContext>(_ => _databaseFixture.GetContext());
var sp = services.BuildServiceProvider();
});
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_databaseFixture?.Dispose();
}
}
base.Dispose(disposing);
_disposed = true;
}
~MySQLAppFactory() => Dispose(false);
}
夹具(底座):
public abstract class DatabaseFixture : IDisposable
{
private readonly object _seedLock = new object();
private bool _disposed = false;
public DbConnection dbConnection { get; protected set; }
protected abstract bool IsInitialized(bool init = false);
protected abstract DbContextOptions<IMAppContext> GetBuildOptions();
// Note that DbContext instances created in this way should be disposed by the
// calling code (typically with a using {} block).
public MyTestContext GetContext(DbTransaction transaction = null)
{
var context = new MyTestContext(GetBuildOptions());
if (transaction != null)
{
context.Database.UseTransaction(transaction);
}
return context;
}
protected void Seed()
{
// lock and seed here
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// dispose managed state (managed objects) that implement IDisposable
dbConnection?.Dispose();
}
// free unmanaged resources (unmanaged objects) and override a finalizer below.
////if anything exists here then the finalizer is required
// set large fields to null to help release them faster.
_disposed = true;
}
}
}
夹具(MySQLDerived - 我也使用其他):
public sealed class MySQLFixture : DatabaseFixture
{
private bool _initialised = false;
private bool _disposed = false;
private readonly string _connectionString;
private readonly string _databaseName;
public MySQLFixture()
{
_databaseName = "some name possibly derived from config or guid etc";
_connectionString = "using _databasename and possibly config, build or environment etc";
dbConnection = new MySqlConnection(_connectionString);
Seed();
}
protected override bool IsInitialized(bool init = false)
{
if (!init)
{
return _initialised;
}
else
{
_initialised = init;
return _initialised;
}
}
protected override DbContextOptions<IMAppContext> GetBuildOptions()
{
return new DbContextOptionsBuilder<IMAppContext>().UseMySQL(dbConnection).Options;
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// dispose managed state (managed objects) that implement IDisposable
}
// free unmanaged resources (unmanaged objects) and override a finalizer below.
////if anything exists here then the this.finalizer is required below
dbConnection.Open();
var command = dbConnection.CreateCommand();
command.CommandText = $"DROP SCHEMA IF EXISTS `{_databaseName.ToLower()}`";
command.ExecuteNonQuery();
// set large fields to null to help release them faster.
}
base.Dispose(disposing);
_disposed = true;
}
~MySQLFixture() => Dispose(false);
}