【问题标题】:How to setup SQLite in WebApplicationFactory?如何在 WebApplicationFactory 中设置 SQLite?
【发布时间】:2020-11-29 14:13:27
【问题描述】:

我正在为我的 ASP.NET Core MVC 应用程序编写集成测试。测试即将向控制器发送POST 请求,然后检查数据库是否正确更新。

我有一个CustomWebApplicationFactory,我试图在其中配置 SQLite 内存数据库,但可能我做错了什么。

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    private SqliteConnection Connection;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        Connection = new SqliteConnection("DataSource=:memory:");
        Connection.Open();

        builder.UseEnvironment("Development");

        builder.ConfigureTestServices(services =>
        {
            // Unregister existing database service (SQL Server).
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<AppDbContext>));

            if (descriptor != null) services.Remove(descriptor);

            // Register new database service (SQLite In-Memory)
            services.AddDbContext<AppDbContext>(options => options.UseSqlite(Connection));
        });
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        Connection.Close();
    }
}

我的测试如下所示:

public class OrderControllerTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
    private readonly HttpClient _httpClient;
    private readonly AppDbContext _context;

    public OrderControllerTests(CustomWebApplicationFactory<Startup> factory)
    {
        _httpClient = factory.CreateDefaultClient();

        var scopeFactory = factory.Services.GetService<IServiceScopeFactory>();

        using var scope = scopeFactory.CreateScope();

        _context = scope.ServiceProvider.GetService<AppDbContext>();
    }

    [Fact]
    public async Task Create_Post_OrderIsCreated()
    {
        // ...
        _context.Customers.Add(customer);
        _context.SaveChanges();
       // ...
    }
}

当我运行测试时,_context.Customers.Add(customer); 行触发了CustomWebApplicationFactory.Dispose() 方法,我得到一个错误:

System.ObjectDisposedException:无法访问已释放的上下文实例。此错误的一个常见原因是释放从依赖注入中解析的上下文实例,然后尝试在应用程序的其他地方使用相同的上下文实例。如果您在上下文实例上调用“Dispose”或将其包装在 using 语句中,则可能会发生这种情况。如果你使用依赖注入,你应该让依赖注入容器负责处理上下文实例。

对象名称:'AppDbContext'。

错误信息描述性很强,但我不知道如何解决。为什么要释放数据库上下文?

【问题讨论】:

    标签: c# sqlite integration-testing asp.net-core-3.1 in-memory-database


    【解决方案1】:

    我想我已经弄清楚了,问题是_context 仅在_scope 中可用,所以我删除了using,现在我在类测试之间共享数据库。每个测试还可以选择清空和填充数据库。

    private readonly HttpClient _httpClient;
    private readonly AppDbContext _context;
    private readonly IServiceScope _scope;
    
    public OrderControllerTests(CustomWebApplicationFactory<Startup> factory)
    {
        _httpClient = factory.CreateDefaultClient();
        _scope = (factory.Services.GetRequiredService<IServiceScopeFactory>()).CreateScope();
        _context = _scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // database is now shared across tests
        _context.Database.EnsureCreated();
    }
    

    【讨论】:

    • 而不是在构造函数中创建作用域服务然后从测试中访问我一直在注入工厂(存储为private readonly CustomWebApplicationFactory&lt;Startup&gt; _factory;然后在测试中我使用工厂创建一个范围,以从每个测试中访问一个范围内的服务(例如 dbcontext)。由于WebApplicationFactory 是一个类或集合夹具,这些测试共享数据库。 WebApplicationFactory 中的构造函数正确打开了数据库连接,只有在 WebApplicationFactory 被释放时才会关闭它。
    • @DefinedRisk 你能分享一个你的解决方案的例子吗?
    【解决方案2】:

    @Muflix 我无法在您的评论问题太长之后添加评论,所以这里是...

    我正在使用 xUnit 和 Shouldly:

    而不是在构造函数中创建作用域服务然后从测试中访问我一直在注入工厂(存储为private readonly CustomWebApplicationFactory&lt;Startup&gt; _factory;)然后在测试中我使用工厂创建一个作用域并访问一个作用域来自每个测试的服务(例如dbcontext)。由于WebApplicationFactoryclass fixturecollection 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);
    }
    

    【讨论】:

    • 非常有趣,谢谢你:-)
    猜你喜欢
    • 2014-11-16
    • 2019-03-27
    • 1970-01-01
    • 1970-01-01
    • 2022-11-06
    • 2014-02-06
    • 2013-01-05
    • 2017-06-16
    • 2019-05-03
    相关资源
    最近更新 更多