(此文章同时发表在本人微信公众号“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);
}
}
}
}

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

  1. 浅谈 EF CORE 迁移和实例化的几种方式

    出于学习和测试的简单需要,使用 Console 来作为 EF CORE 的承载程序是最合适不过的.今天笔者就将平时的几种使用方式总结成文,以供参考,同时也是给本人一个温故知新的机会.因为没有一个完整的 ...

  2. 浅谈.Net Core后端单元测试

    目录 1. 前言 2. 为什么需要单元测试 2.1 防止回归 2.2 减少代码耦合 3. 基本原则和规范 3.1 3A原则 3.2 尽量避免直接测试私有方法 3.3 重构原则 3.4 避免多个断言 3 ...

  3. [翻译] EF Core in Action 关于这本书

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  4. 深入理解 EF Core:EF Core 读取数据时发生了什么?

    阅读本文大概需要 11 分钟. 原文:https://bit.ly/2UMiDLb 作者:Jon P Smith 翻译:王亮 声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的.其中可能 ...

  5. 深入理解 EF Core:EF Core 写入数据时发生了什么?

    阅读本文大概需要 14 分钟. 原文:https://bit.ly/2C67m1C 作者:Jon P Smith 翻译:王亮 声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的.其中可能 ...

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

    从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和 ...

  7. 如何使用ASP.NET Core、EF Core、ABP(ASP.NET Boilerplate)创建分层的Web应用程序(第一部分)

    本文是为了学习ABP的使用,是翻译ABP官方文档的一篇实战教程,我暂时是优先翻译自己感兴趣或者比较想学习的部分,后续有时间希望能将ABP系列翻译出来,除了自己能学习外,有可能的话希望帮助一些英文阅读能 ...

  8. EF Core中避免贫血模型的三种行之有效的方法(翻译)

    Paul Hiles: 3 ways to avoid an anemic domain model in EF Core 1.引言 在使用ORM中(比如Entity Framework)贫血领域模型 ...

  9. 【ASP.NET Core】EF Core - “影子属性” 深入浅出经典面试题:从浏览器中输入URL到页面加载发生了什么 - Part 1

    [ASP.NET Core]EF Core - “影子属性”   有朋友说老周近来博客更新较慢,确实有些慢,因为有些 bug 要研究,另外就是老周把部分内容转到直播上面,所以写博客的内容减少了一点. ...

随机推荐

  1. PHP的Tag标签

    PHP的标签有至少四种:<?php ?>.<?= ?>.<script language="php"></script>.<? ...

  2. git rebase简介(高级篇)

    原文:http://gitbook.liuhui998.com/4_3.html 一.基本   对于git rebase,你亦可以选择进行交互式的rebase.这种方法通常用于在向别处推送提交之前对它 ...

  3. 《oracle每日一练》oralce数据库的导入导出

    今天尝试了数据库的导出,直接在命令行里面使用了导出指令: exp uname/pwd@127.0.0.1:1521/orcl  file='xx.dmp' 出现的问题: 直接@tnsnames里面配的 ...

  4. C#之键值对

    1.初始化一个键值对 //初始化定义一个键值对,注意最后的括号 Dictionary<int, string> dic = new Dictionary<int, string> ...

  5. mysql性能优化学习笔记-存储引擎

    mysql体系架构 客户端(java.php.python等) mysql服务层(连接管理器.查询解析器.查询优化器.查询缓存) mysql存储引擎(innodb.myisam等) 存储引擎针对表而言 ...

  6. google API 点连线

    这个是模拟的数据,用于测试,为了方便学习 弹出框信息都是固定的,以及操作都不是写的循环,实际开发用 setInterval 或者for 以减少冗余. <!DOCTYPE html> < ...

  7. 【python】入门学习(十)

    #入门学习系列的内容均是在学习<Python编程入门(第3版)>时的学习笔记 统计一个文本文档的信息,并输出出现频率最高的10个单词 #text.py #保留的字符 keep = {'a' ...

  8. java中方法参数的一些总结(1)

    1.问题说明        在C++中,函数调用时有传值调用和传址调用两种方式,但在Java中只有传值调用一种方式.Java中的方法参数为那几种基本数据类型的情况跟C++中一样,传入的只是变量的拷贝. ...

  9. 使用rdesktop连接Windows远程桌面

    rdesktop 使用简单,windows也不和装什么服务端,是要把远程桌面共享打开就行了 安装 yum -y install rdesktop 具体使用方法要先打开终端,然后输入以下命令: rdes ...

  10. java判断身份证有效性

    import java.util.Calendar; public class CertNoUtil { public static boolean vIDNumByRegex(String idNu ...