(此文章同时发表在本人微信公众号“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. Print Common Nodes in Two Binary Search Trees

    Given two Binary Search Trees, find common nodes in them. In other words, find intersection of two B ...

  2. Androidmanifest之manifest标签详细介绍

    http://www.haogongju.net/art/2094337 文档下载

  3. codeforces 591A. Wizards' Duel 解题报告

    题目链接:http://codeforces.com/problemset/problem/591/A 题目意思:其实看下面这幅图就知道题意了,就是Harry 和 He-Who-Must-Not-Be ...

  4. 【leetcode】Restore IP Addresses (middle)

    Given a string containing only digits, restore it by returning all possible valid IP address combina ...

  5. Android Studio新建了一个项目提示Error:Unable to start the daemon process

    提示如下错误:

  6. objective-c可变数组

     1 #pragma mark ---------------可变数组-----------------  2 //        可以在数组里面进行增删改的操作  3 //  4 //        ...

  7. iOS-运行时机制

    这里的两篇运行时的文章感觉还不错. 收藏: 初识iOS运行时RunTime | // TODO: http://www.saitjr.com/ios/objc-runtime.html Objecti ...

  8. September 9th 2016 Week 37th Friday

    Within you, I lose myself. 有了你,我迷失了自我. I never had such feeling, maybe just because I never invested ...

  9. ios框架

    iPhone OS(现在叫iOS)是iPhone, iPod touch 和 iPad 设备的操作系统.        1,Core OS: 是用FreeBSD和Mach所改写的Darwin, 是开源 ...

  10. 使用DateUtils和DateFormatUtils处理时间日期转换与SimpleDateFormat的区别

    在Apache Commons项目的Lang里面,有两个类:DateUtils和DateFormatUtils,专门用于处理时间日期转换.它们在 org.apache.commons.lang.tim ...