单元测试 DbContext

55
我研究了一些关于如何对 DbContext 进行单元测试的技术。我希望向上下文添加一些内存数据,以便我的测试可以针对它运行。我正在使用 Database-First 方法。
我找到的两篇最有用的文章是 thisthis。这种方法依赖于创建一个 IContext 接口,MyContext 和 FakeContext 都将实现它,从而允许 Mock 上下文。
然而,我正在尝试避免使用仓库来抽象 EF,因为 pointed by some 人指出,自 EF 4.1 以来,已经通过 DbSet 和 DbContext 实现了仓库和工作单元模式,我真的很想保留由 EF 团队实现的所有功能,而不必使用通用仓库自己维护它们,就像我在其他项目中做过的那样(那真的很痛苦)。

我使用IContext会走相同的路线(是这样吗?)。

我考虑创建一个继承自主要MyContext的FakeContext,从而利用其下面的DbContext在不访问数据库的情况下运行我的测试。 我找不到类似的实现,所以希望有人能帮助我。

我做错了什么,或者这可能会导致我没有预料到的问题吗?


对于仍在寻找答案的任何人 - 我编写了一个简单的库,以非常简单的方式来方便地模拟 DbContext。有关详细信息,请参阅我在SO上对类似问题的其他回答:https://dev59.com/aV8e5IYBdhLWcg3wNYUn#33716219。附言 - 我同意Ladislav Mrnka所说的,但是我认为在某些情况下,你需要模拟你的DbSet并对其进行单元测试是不可避免的。尽管如此,你需要记住,你不应该测试你的LINQ查询以验证它们是否返回正确的数据。这就是集成测试更适用的地方。 - niaher
4个回答

38

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

你提到了FakeContext和模拟上下文 - 为什么要同时使用两者?这些只是实现提供测试的不同方式。

还有一个更大的问题 - 伪造或模拟上下文或设置只有一个结果:你不再测试真正的代码了。

一个简单的例子:

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,因此可以调用本地的.NET方法,这些方法无法转换为SQL,还有许多其他在LINQ to Entities中不可用的LINQ功能!您还可以找到其他数据修改问题——引用完整性、级联删除等。这就是我认为处理context/LINQ to Entities的代码应该覆盖集成测试并针对真实数据库执行的原因。


17
关于更喜欢进行集成测试的问题,我不同意这是使用EF测试代码的唯一方式。我认为能够进行两种测试方式都很重要。当单元测试业务逻辑使用EF时,您不希望通过执行集成测试来测试您的模型是否与数据库同步。这是单元测试的定义。显然,集成测试很重要,但它是一种独立的测试类型。我已经撰写了另一种解答,说明如何进行单元测试。 - Josh Gallagher
4
我已经阅读了很多关于如何模拟ObjectContext的问答,但所有的答案都感觉“不完整”。有了Ladislav的回答,我现在知道了问题所在。你非常准确地指出了它:“还有一个更大的问题-伪造或模拟上下文或集合只有一个结果:您不再测试真正的代码。”---不仅是我的问题!!EF有太多微小的细节,我实际上想要测试他们的ObjectContext实现,因为偶尔会在他们这边遇到一些非常愚蠢的东西或缺失的功能,没有人会想到他们会缺失..谢谢 - quetzalcoatl
3
真的吗?你无法想象嘲笑 DB 会有任何用处的情况吗?这些特定的测试不会告诉你关于 LINQ-to-SQL 错误的信息,但它们可以测试其他错误。 - heneryville
2
@heneryville:当然会!但是,将代码编写为明确界定必须由集成测试覆盖的代码和应该由单元测试覆盖的代码之间的边界,这不是更好吗?在这样的代码中,您不会伪造上下文或设置,因为其使用将被另一个负责执行查询的类隐藏。那个类将被集成测试覆盖,并且在其他代码中使用它将在单元测试中伪造。 - Ladislav Mrnka
3
跟你的电子邮件服务类比,就好像不运行数据库服务器一样,在单元测试代码时你并不需要关心它。使用虚拟数据上下文和集合的问题在于会默默地替换 Linq 提供程序。除非你也使用真正的数据库提供程序对代码进行测试,否则你根本没有测试过你的 Linq 查询。这就是整个意图所在。通过重构代码,可以更好地分离出来,使你的虚拟数据隐藏查询(而不仅仅是集合或上下文),而查询本身将被单独测试。 - Ladislav Mrnka
显示剩余8条评论

14

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

http://effort.codeplex.com

一个小预告:

您无需添加任何样板代码,只需简单调用库的适当API即可,例如:

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

一开始可能看起来像魔法,但创建的ObjectContext对象将与内存数据库通信,完全不会与原始真实数据库交互。术语“短暂”是指此数据库的生命周期,在创建的ObjectContext对象存在期间才存在。同时创建的ObjectContext对象与专用数据库实例通信,数据不会在它们之间共享。这使得编写自动化测试变得容易。

该库提供了各种功能来自定义创建:跨实例共享数据、设置数据库的初始数据、在不同数据层上创建虚拟数据库......请查看项目网站获取更多信息。


11

从EF 4.3版本开始,在创建上下文之前,您可以通过注入一个伪造的DefaultConnectionFactory单元测试您的代码


1
这很有趣。我得试试看。 - Ladislav Mrnka
@LadislavMrnka,我可以问一下您对这种技术的看法吗? - Chris
@ChrisPaynter:这是一个选择。我不确定你会不会选择它,但肯定比伪造上下文或设置要好。 - Ladislav Mrnka

5
Entity Framework 4.1接近于可以在测试中模拟,但需要额外的努力。T4模板为您提供了一个包含DbSet属性的DbContext派生类。我认为您需要模拟的两个方面是这些属性返回的DbSet对象和您在DbContext派生类上使用的属性和方法。通过修改T4模板,两者都可以实现。
Brent McKendrick在这篇文章中展示了需要进行的修改类型,但没有提供可以实现此目的的T4模板修改。大致上,这些修改如下:
  1. 将DbContext派生类上的DbSet属性转换为IDbSet属性。
  2. 添加一节生成DbContext派生类接口的内容,其中包含IDbSet属性和任何其他您需要模拟的方法(例如SaveChanges)。
  3. 在DbContext派生类中实现新接口。

6
但是这恰恰是错误和无用的。如果您嘲弄设置/上下文并使用我答案中的示例,您将获得绿色的单元测试,但在运行时会出现异常。它不会来自数据库的异常 - 它将来自实体框架 = 您浪费了时间编写测试,测试了无关紧要的内容,并且对您的代码毫无帮助。那么这样的测试有什么价值,它测试了什么?此外,许多人遵循的规则是,您不应嘲弄您不拥有的代码,因为在这种情况下,您只是猜测其功能。 - Ladislav Mrnka
10
针对您的例子,如果您在单元测试和集成测试中获得了不一致的结果,并意识到您使用了一个无效的技术。但是,如果您使用更基本的LINQ来检索实体并对其执行一些业务逻辑,则仍然可以将该业务逻辑测试为单元测试。例如,将您的示例更改为两个步骤执行操作:1)通过LINQ检索项目(可能是 .ToList),2)使用您的方法去除列表项。然后单元测试仍然很有用,而集成测试不必覆盖所有边缘情况。 - Josh Gallagher
6
当然,你的方法在某些情况下是有效的,但单元测试策略必须在所有情况下都有效 - 为了使其起作用,你必须将 EF 和 IQueryable 完全从被测试的方法中移除。单元测试不是为了假装你正确地编写了它 - 测试的目的是验证你是否正确地编写了逻辑。此外,在你的场景中,编写单元测试和集成测试来验证相同的代码并不是一个好的实践。 - Ladislav Mrnka
@Ladislav:您能否详细说明一下如何从BL代码中精确提取EF代码并将其放在单独的位置? 我正在考虑这样一种情况,在这种情况下,BL方法包含对“entities.something”进行多个不同调用,并且最后有“entities.SaveChanges”... 我已经在此处发布了我的问题。 - John Miner
需要考虑实际情况。我已经做了两年了,从中获得了很多价值,其中包括:a)为EF和IQueryable逻辑编写测试;b)不需要让这些测试处理包含数据库的测试的复杂性,例如数据清理、执行时间较慢、无法并行执行测试等。 - Josh Gallagher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接