再談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);
                }
            }
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM