单元测试 DbContext

Posted

技术标签:

【中文标题】单元测试 DbContext【英文标题】:Unit Testing DbContext 【发布时间】:2011-10-09 15:17:26 【问题描述】:

我研究了一些有关可用于对 DbContext 进行单元测试的技术的信息。我想在上下文中添加一些内存中数据,以便我的测试可以针对它运行。我正在使用数据库优先的方法。

我发现最有用的两篇文章是this 和this。 这种方法依赖于创建 MyContext 和 FakeContext 都将实现的 IContext 接口,从而允许模拟上下文。

但是,我试图避免使用存储库来抽象 EF,例如 pointed by some 人,因为 EF 4.1 已经通过 DbSet 和 DbContext 实现了存储库和工作单元模式,我真的很想保留 EF 团队实现的所有功能,而不必像我在其他项目中那样使用通用存储库自己维护它们(这有点痛苦)。

使用 IContext 会引导我走上相同的道路(或者不会?)。

我考虑过创建一个从主 MyContext 继承的 FakeContext,从而利用它下面的 DbContext 来运行我的测试而无需访问数据库。 我找不到类似的实现,所以我希望有人可以帮助我。

我是不是做错了什么,或者这会导致我遇到一些我没有预料到的问题?

【问题讨论】:

对于仍在寻找答案的任何人 - 我编写了一个简单的库来帮助以非常简单的方式模拟 DbContext。有关详细信息,请参阅我对类似问题的其他答案:***.com/a/33716219/111438。 PS - 我同意 Ladislav Mrnka 的说法,但是我认为在某些情况下,你需要模拟你的 DbSet 并针对它运行单元测试是不可避免的。尽管您需要记住,您不应该测试您的 LINQ 查询来验证它们是否返回正确的数据。这就是集成测试更适用的地方。 【参考方案1】:

问自己一个问题:你要测试什么?

您提到了 FakeContext 和 Mocking the context - 为什么要同时使用两者?这些只是执行相同操作的不同方法 - 提供上下文的仅测试实现。

还有一个更大的问题 - 伪造或模拟上下文或集合只有一个结果:您不再测试您的真实代码。

简单示例:

public interface IContext : IDisposable

    IDbSet<MyEntity> MyEntities  get; 


public class MyEntity

    public int Id  get; set; 
    public string Path  get; set;  


public class MyService

    private bool MyVerySpecialNetMethod(e)
    
        return File.Exists(e.Path);
    

    public IEnumerable<MyEntity> GetMyEntities()
    
        using (IContext context = CreateContext())
         
            return context.MyEntities
                .Where(e => MyVerySpecialNetMethod(e))
                .Select(e)
                .ToList();
        
    

现在假设您的 SUT 中有这个(被测系统 - 在单元测试的情况下,它是一个单元 = 通常是一个方法)。在您提供FakeContextFakeSet 的测试代码中,它会起作用-您将获得绿色测试。现在在生产代码中,您将提供另一个派生的DbContextDbSet,您将在运行时遇到异常。

为什么?因为通过使用FakeContext,您还更改了 LINQ 提供程序,而不是 LINQ to Entities,您正在运行 LINQ to Objects,因此调用无法转换为 SQL 的本地 .NET 方法以及 LINQ 中不可用的许多其他 LINQ 功能实体!您还可以在数据修改中发现其他问题 - 引用完整性、级联删除等。这就是为什么我认为处理上下文/LINQ to Entities 的代码应该包含在集成测试中并针对真实数据库执行的原因。

【讨论】:

Ladislav - 你知道如何在所描述的情况下运行(和自动化)集成测试的任何好的资源吗? 关于更喜欢进行集成测试,我不同意这是测试使用 EF 的代码的唯一方法。我相信能够同时做到这两点很重要。在对碰巧使用 EF 的业务逻辑进行单元测试时,您不希望通过集成测试来测试您的模型是否与数据库同步。这是单元测试的定义。显然集成测试很重要,但它是一种单独的测试。我写了一个替代答案,解释了如何进行单元测试。 我已经阅读了很多关于如何模拟 ObjectContext 的问答,但他们都觉得“不完整”。有了拉迪斯拉夫的回答,我现在知道气味在哪里了。您已经很好地指出了这一点:“还有一个更大的问题 - 伪造或模拟上下文或集合只有一个结果:您不再测试您的真实代码。” --- 不只是我的!! EF 有很多复杂的东西,我实际上确实想测试他们的 ObjectContext 实现,因为我时不时会偶然发现他们身边如此愚蠢的东西,或者一些没人会认为丢失的缺失功能。谢谢 真的吗?你能想象一个模拟数据库有用的情况吗?这些特定测试不会告诉您有关 LINQ-to-SQL 错误的信息,但它们会测试其他错误。 @WiktorZychla:类似于您的电子邮件服务没有运行数据库服务器,这当然是您在对代码进行单元测试时不会打扰的事情。伪造上下文和集合的问题在于默默地替换 Linq 提供程序。除非您使用真正的数据库提供程序测试您的代码,否则您根本没有测试您的 Linq 查询。这就是重点。通过重构代码,您可以获得更好的分离,您的假将隐藏查询(不仅仅是设置或上下文)并且查询本身将单独测试。【参考方案2】:

我正在开发一个开源库来解决这个问题。

http://effort.codeplex.com

一个小预告:

您不必添加任何样板代码,只需调用该库的相应 API,例如:

var context = Effort.ObjectContextFactory.CreateTransient<MyContext>();

起初这似乎很神奇,但创建的 ObjectContext 对象将与in-memory database 通信,并且根本不会与原始真实数据库通信。术语“瞬态”指的是这个数据库的生命周期,它只存在于创建的 ObjectContext 对象存在期间。并发创建的 ObjectContext 对象与专用数据库实例通信,数据不会在它们之间共享。这样可以轻松编写自动化测试。

该库提供了各种功能来自定义创建:跨实例共享数据、设置数据库的初始数据、在不同的数据层上创建假数据库...查看project site 了解更多信息。

【讨论】:

【参考方案3】:

从 EF 4.3 开始,您可以通过在创建上下文之前注入虚假的 DefaultConnectionFactory 来 unit test your code。

【讨论】:

这很有趣。我得试试看。 @LadislavMrnka 我能问一下您对这种技术的看法吗? @ChrisPaynter:这是一个选择。我不确定你是否会选择它,但它绝对比伪造上下文或设置更好。【参考方案4】:

Entity Framework 4.1 几乎可以在测试中进行模拟,但需要一些额外的努力。 T4 模板为您提供了一个包含 DbSet 属性的 DbContext 派生类。我认为您需要模拟的两件事是这些属性返回的 DbSet 对象以及您在 DbContext 派生类上使用的属性和方法。两者都可以通过修改T4模板来实现。

Brent McKendrick 已经在this post 中展示了需要进行的修改类型,但没有展示可以实现此目的的 T4 模板修改。大致是:

    将 DbContext 派生类上的 DbSet 属性转换为 IDbSet 属性。 添加一个部分,为 DbContext 派生类生成一个接口,其中包含 IDbSet 属性和您需要模拟的任何其他方法(例如 SaveChanges)。 在 DbContext 派生类中实现新接口。

【讨论】:

但这正是错误和无用的。如果您模拟集合/上下文并使用我的答案中的示例,您将获得绿色单元测试,但在运行时出现异常。它不会是数据库的例外 - 它将是实体框架的例外=您浪费时间编写测试,该测试什么都不测试,也没有说明您的代码。那么这种测试的价值是什么,它测试了什么?此外,许多人遵循的规则是你不应该模拟你不拥有的代码,因为在这种情况下你只是在模拟它时猜测它的功能。 是的,对于您的示例,您将在单元测试和集成测试之间得到不一致的结果,并意识到您使用了一种不起作用的技术。但是,如果您使用更多 vanilla LINQ 来检索实体,然后使用它们执行一些业务逻辑,那么在单元测试中测试该业务逻辑仍然非常有效。例如。更改您的示例以分两步执行操作:1)通过 LINQ(可能是 .ToList)检索项目,2)使用您的方法剥离列表。那么单元测试仍然有用,集成测试不必涵盖所有边缘情况。 当然,您的方法在某些情况下有效,但单元测试策略必须在所有情况下都有效 - 要使其有效,您必须将 EF 和 IQueryable 完全从您的测试方法中移出。这不是假装你写正确的单元测试点 - 测试点是验证你是否正确编写了逻辑。此外,编写单元测试和集成测试来验证在您的场景中必须完成的相同代码也不是一个好习惯。 @Ladislav:您能否详细说明应该如何从 BL 代码中提取 EF 代码并将其放在单独的位置?我正在考虑 BL 方法包含对“entities.something”的多个不同调用并且最后有“entities.SaveChanges”的情况......我已经发布了我的问题HERE 有一些实用性需要考虑。我已经这样做了两年了,并且从 a) 对 EF 和 IQueryable 逻辑进行测试,以及 b) 没有这些测试需要处理包括数据库的测试的复杂性,例如数据清理、执行时间较慢、缺乏并行执行测试。

以上是关于单元测试 DbContext的主要内容,如果未能解决你的问题,请参考以下文章

在单元测试期间避免在EF Core 2.2中使用HasData Seed DbContext

使用Moq进行单元测试 - 值不能为空

将 AddOrUpdate 与 Entity Framework 6 Repository 一起使用的单元测试方法

Xunit 内存数据库中的新 DBContext 类具有实时数据

Django单元测试测试视图问题,怎么解决

.NET 单元测试的艺术&单元测试之道C#版