再谈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; }
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

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);
                }
            }
        }
    }
时间: 2024-10-17 12:36:54

再谈EF Core内存数据库单元测试问题的相关文章

再谈Segmentation fault (core dumped)问题 -查找段错误原因

再谈Segmentation fault (core dumped)问题 -查找段错误原因    在前一篇文章"Segmentation fault (core dumped) "有说了具体core dumped产生的原因. 下面主要来介绍下问题的解决与查找,在linux下一般都使用gdb进行调试,那今天我就以Ubuntu 14.04环境作为介绍 来查找正在的core dumped的原因.需要说明的是,你在编译程序的时候要加调试选项 -g. $ gcc -o app reverse.c

从一张图开始,谈一谈.NET Core和前后端技术的演进之路

从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和前后端分离那些事儿>上,我们曾经试图通过一系列抽丝剥茧的过程来引导大家在这条基于.NET Core的前后端分离有关的技术路线上持续发散,由于各种原因未能成功,因此,技术社区执行主席,李文强同学整理了一张图,并由溪源以专刊的形式来描述他所认为的技术演进之路.  这张图涉及到的知识点,涵盖了目前前后端技术近

再谈ORACLE CPROCD进程

罗列一下有关oprocd的知识点 oprocd是oracle在rac中引入用来fencing io的 在unix系统下,如果我们没有采用oracle之外的第三方集群软件,才会存在oprocd进程 在linux系统下,只有在10.2.0.4版本后,才会具有oprocd进程 在window下,不会存在oprocd 进程,但是会存在一个oraFenceService服务,用来实现相同的功能,该服务采用的技术是基于windows的,与oprocd不同 oprocd进程可以运行在两者模式下:fatal和n

再谈:为什么开源C/C++开源框架极昂贵?

今天读了一篇文章:<腾讯前员工创业笔记:那些跟钱有关的事儿>(http://tech.163.com/14/0515/08/9S9975C5000915BF.html),摘录两段: 刚开工的时候买办公电脑,我心想创业公司应该省钱,就把机箱CPU硬盘内存显卡买来自己装.虽然"科班出身,基本功扎实",但因为缺乏经验,装一台机器要花费足足半天时间.于是我上网百度了一番,发现选择上门装机服务,装一台要100块,装好了送过来要花50块.如果我用这半天时间做点产品设计,亦或写几行代码,

EF Core实践 (使用MS SqlServer)

这里使用 MS SQLSERVER ,网上大多使用 SQLite 先来一个CodeFirst 新建项目 这里我们选择  ASP.NET Core Web Application (.NET Core)  这里选择web 应用程序,然后更改身份验证 改为 不进行身份验证 然后再包管理控制台里执行下面两条命令 引用 EntityFrameworkCore Install-Package Microsoft.EntityFrameworkCore 再引用 EntityFrameworkCore.Sql

.NET Core 1.0、ASP.NET Core 1.0和EF Core 1.0简介

.NET Core 1.0.ASP.NET Core 1.0和EF Core 1.0简介 英文原文:Reintroducing .NET Core 1.0, ASP.NET Core 1.0, and EF Core 1.0 新版本的 ASP.NET 和 Entity Framework 有一个严重的问题,就是它们同以前的版本不兼容.这不只是行为或 API 稍有差异的事,而基本上是进行了完全的重写,去掉了大量的功能. 因此,目前人们认为,将这些框架称为 ASP.NET 5.0 和 Entity

ASP.NET Core 开发-Entity Framework (EF) Core 1.0 Database First

ASP.NET Core 开发-Entity Framework Core 1.0 Database First,ASP.NET Core 1.0 EF Core操作数据库. Entity Framework Core 1.0 也已经发布了,可以适用于 .NET Core 1.0 及ASP.NET Core 1.0 . EF Core RC2 时,使用的Code First: http://www.cnblogs.com/linezero/p/EntityFrameworkCore.html E

boost.log(七)再谈过滤

再谈过滤 我们已经在前面的章节接触过滤,但只是浅尝辄止.我们现在能够添加日志记录并设置接收器的属性,我们需要建立复杂的过滤功能.让我们看下这个例子: #include <string> #include <fstream> #include <iomanip> #include <boost/log/core.hpp> #include <boost/smart_ptr.hpp> #include <boost/log/sinks.hpp&

asp.net core ef core mysql 新增数据并发异常处理

net core 2.0发布后,一直想体验下,因种种原因,一直在拖着没进行. 前阵子公司要加个新的内部管理后台,正好可以用asp.net core来做下,体验下net core的魅力. 啃过文档后就上手了,一切很顺利. 直到周五,出现了一个并发异常的问题,本以为可以很快处理掉的,但没想到一直花费了很长时间才解决掉,现在记录下情况,有相同经历的伙伴以后可以参考. 先上异常截图. 异常提示: Microsoft.EntityFrameworkCore.DbUpdateConcurrencyExcep