• 再谈EF Core内存数据库单元测试问题


    (此文章同时发表在本人微信公众号“dotNET每日精华文章”,欢迎右边二维码来关注。)

    题记:在用EF Core的内存数据库进行单元测试的时候遇到“无法访问已释放的对象”的错误怎么办?

    之前在EF Core 1.0中使用Include的小技巧中简单谈到了使用EF Core内存数据库进行单元测试的方法。不过这个方法有个小问题,就是容易出现“无法访问已释放的对象”的错误。

    在之前的示例代码中(http://git.oschina.net/ike/codes/jtu9dnsk3pe6x24clbq50),单元测试能够顺利通过,是因为db和db2两个实例对象并没有放到using里面让其自动disposed掉。可在大部分情况下,正确的写法应该是使用using的,就算不使用using,如果一次性执行多个测试,那么就会遇到这个“无法访问已释放的对象”错误。

    根据这里的讨论(https://github.com/aspnet/EntityFramework/issues/4092),这一错误的根源是EF Core的内存数据库设计出来的行为和真实的关系数据库是一致的,同时又受到ServiceProvider作用域的控制。换句话说,就是在同一个ServiceProvider之下,内存数据库的数据和关系数据库的数据一样是保持“持久”的(当然是在内存中)。由此导致了,使用内存数据库进行单元测试,需要考虑两种情况:

    1,单元测试涉及的每次数据操作,数据都完全隔离。

    这种情况下,可以采用官方的文档(https://github.com/aspnet/EntityFramework.Docs/issues/95)中的说明,实例化或者注入ServiceCollection,然后每次操作数据库都要通过新的ServiceProvider获得DbContext实例。具体代码如下:

    using Microsoft.Data.Entity;
    using Microsoft.Data.Entity.Infrastructure;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Linq;
    
    namespace ConsoleApp1
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                var serviceCollection = new ServiceCollection();
                serviceCollection
                    .AddEntityFramework()
                    .AddInMemoryDatabase()
                    .AddDbContext<SampleContext>(c => c.UseInMemoryDatabase());
    
    
                using (var db = serviceCollection.BuildServiceProvider().GetService<SampleContext>())
                {
                    db.Blogs.Add(new Blog { Url = "Test" });
                    db.SaveChanges();
                    Console.WriteLine(db.Blogs.Count());
                }
    
                using (var db = serviceCollection.BuildServiceProvider().GetService<SampleContext>())
                {
                    db.Blogs.Add(new Blog { Url = "Test" });
                    db.SaveChanges();
                    Console.WriteLine(db.Blogs.Count());
                }
            }
        }
    
        public class SampleContext : DbContext
        {
            public DbSet<Blog> Blogs { get; set; }
        }
    
        public class Blog
        {
            public int BlogId { get; set; }
            public string Url { get; set; }
        }
    }

    2,单元测试涉及的每次数据操作,数据需要模拟真实的唯一数据库。

    详细说来,就是我需要在一个TestFixture中,先插入一些示例数据,然后随后的每个单元测试都以这些示例数据为准。在这种情况下,为了避免遇到“无法访问已释放的对象”,就需要通过统一的ServiceProvider(上面已经解释了为什么需要统一的)来获得在Scoped中分别获取DbContext以避免之前的DbContext被释放。这种方式,官方也有参考的代码,具体我就不贴了,见(https://github.com/aspnet/MusicStore/blob/dev/src/MusicStore/Models/SampleData.cs)。下面是我给出的一个示例代码(代码片段见:http://git.oschina.net/ike/codes/t069no34dfu8p2zeahrsv):

        public class EFCoreInMemoryTest
        {
            [Fact]
            public async Task WillSuccessWithWrongApproach()
            {
                var serviceCollection = new ServiceCollection();
                serviceCollection
                    .AddEntityFramework()
                    .AddInMemoryDatabase()
                    .AddDbContext<MarketDbContext>(options => options.UseInMemoryDatabase());
    
                var serviceProvider = serviceCollection.BuildServiceProvider();
    
                var db = serviceProvider.GetRequiredService<MarketDbContext>();
                SampleData.Create(db);
    
                var db2 = serviceProvider.GetRequiredService<MarketDbContext>();
    
                var products = await db2.Products.ToListAsync();
    
                Assert.Equal(3, products.Count);
    
                var promotions = await db2.Promotions.ToListAsync();
    
                Assert.Equal(2, promotions.Count);
            }
    
            [Fact]
            public async Task WillFaild()
            {
                var serviceCollection = new ServiceCollection();
                serviceCollection
                    .AddEntityFramework()
                    .AddInMemoryDatabase()
                    .AddDbContext<MarketDbContext>(options => options.UseInMemoryDatabase());
    
                var serviceProvider = serviceCollection.BuildServiceProvider();
    
                using (var db = serviceProvider.GetRequiredService<MarketDbContext>())
                {
                    SampleData.Create(db);
                }
    
                using (var db2 = serviceProvider.GetRequiredService<MarketDbContext>())
                {
                    var products = await db2.Products.ToListAsync();
    
                    Assert.Equal(3, products.Count);
    
                    var promotions = await db2.Promotions.ToListAsync();
    
                    Assert.Equal(2, promotions.Count);
                }
    
            }
    
            [Fact]
            public async Task WillSuccessWithRightApproach()
            {
                var serviceCollection = new ServiceCollection();
                serviceCollection
                    .AddEntityFramework()
                    .AddInMemoryDatabase()
                    .AddDbContext<MarketDbContext>(options => options.UseInMemoryDatabase());
    
                var serviceProvider = serviceCollection.BuildServiceProvider();
    
                DoDbActionInScoped(serviceProvider, (db) =>
                {
                    SampleData.Create(db);
                });
    
                await DoDbActionInScopedAsync(serviceProvider, async (db2) =>
                {
                    var products = await db2.Products.ToListAsync();
    
                    Assert.Equal(3, products.Count);
    
                    var promotions = await db2.Promotions.ToListAsync();
    
                    Assert.Equal(2, promotions.Count);
                });
            }
    
            private void DoDbActionInScoped(IServiceProvider serviceProvider, Action<MarketDbContext> action)
            {
                using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
                {
                    using (var db = serviceScope.ServiceProvider.GetRequiredService<MarketDbContext>())
                    {
                        action(db);
                    }
                }
            }
    
            private async Task DoDbActionInScopedAsync(IServiceProvider serviceProvider, Func<MarketDbContext, Task> action)
            {
                using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
                {
                    using (var db = serviceScope.ServiceProvider.GetRequiredService<MarketDbContext>())
                    {
                        await action(db);
                    }
                }
            }
        }
  • 相关阅读:
    Oracle notes
    jQuery笔记
    sql developer 要求enter the full pathname for java.exe
    [Error] WCF: SOAP security negotiation
    Unity
    Windows Form 开发笔记
    WP开发 资料整理
    乔迁新居:http://longwarelive.spaces.live.com/
    2008年1月1日启用 longware@live.cn
    《程序员》杂志揭晓2007软件中国年度评选
  • 原文地址:https://www.cnblogs.com/redmoon/p/5274273.html
Copyright © 2020-2023  润新知