多个Entity framework dbcontexts共用一个数据库的集成测试

7
在我的应用程序中,我有多个小的实体框架数据库上下文共享同一个数据库,例如:
public class Context1 : DbContext {
    public Context1()
        : base("DemoDb") {
    }
}

public class Context2 : DbContext {
    public Context2()
        : base("DemoDb") {
    }
}

所有数据库更新都是通过脚本完成的,不依赖迁移(今后也不会)。问题是 - 如何对这些上下文进行集成测试?

我认为有三个选项(可能还有其他选项,我只是不知道)

选项1 - 超级上下文 - 包含设置数据库所需的所有模型和配置的上下文:

public class SuperContext : DbContext
{
    public SuperContext()
        : base("DemoDb") {
    }
}

在这个选项中,测试数据库将根据超级上下文设置,并且所有随后的测试都将通过更小的上下文进行。 我不喜欢这个选项的原因是我将重复所有已经构建的配置和实体模型。 选项2 - 为集成测试创建一个定制的初始化程序,以运行所有适当的数据库初始化脚本:
public class IntegrationTestInitializer : IDatabaseInitializer<DbContext> {

    public void InitializeDatabase(DbContext context) {
        /* run scripts to set up database here */
    }
}

这个选项允许针对真实的数据库结构进行测试,但也需要在每次添加新的数据库脚本时更新。

选项3 - 只测试各个上下文:

在此选项中,只需让EF基于上下文创建测试数据库,所有测试都将在其自己的“沙盒”中运行。我不喜欢这种方法的原因是它感觉就像你没有针对真实的数据库进行测试。

我目前倾向于选项2。你们认为有更好的方法吗?

2个回答

8
我经常使用集成测试,因为我认为当涉及到依赖数据的过程时,这是最可靠的测试方式。我还有一些不同的上下文以及用于数据库升级的DDL脚本,所以我们的情况非常相似。
我最终采用的是“选项4”:通过常规用户界面来维护单元测试数据库内容。当然,大多数集成测试会在测试的“执行”阶段(稍后再说)临时修改数据库内容,但是在测试会话开始时并未设置内容。
原因如下:
在某个阶段,我们还在测试会话开始时生成数据库内容,无论是通过代码还是反序列化XML文件。(我们还没有EF,但否则我们可能会在数据库初始化器中有一些"Seed"方法)。逐渐地,我开始对这种方法感到疑虑。当数据模型或业务逻辑发生变化时,维护代码/XML非常困难,特别是当必须设计新用例时。有时我允许自己对这些测试数据进行轻微破坏,因为我知道它不会影响测试。
此外,数据必须有意义,即它们必须像来自真实应用程序的数据一样有效和连贯。确保这一点的方法之一是让应用程序本身生成数据,否则您将不可避免地在Seed方法中复制业务逻辑。模拟真实世界的数据实际上非常困难。这是我发现的最重要的事情。测试不代表真实用例的数据组合不仅浪费时间,而且是虚假的安全保障。
因此,我通过应用程序的前端创建测试数据,然后仔细将这些内容序列化为XML或编写能够完全生成相同内容的代码。直到有一天,我突然想到这些数据已经在数据库中准备好了,所以为什么不直接使用它们呢?
现在也许你会问如何使测试独立? 集成测试和单元测试一样,应该能够独立执行。它们不应该依赖于其他测试,也不应该受到其他测试的影响。我假设你提问的背景是为每个集成测试创建并初始化一个数据库。这是实现独立测试的一种方式。
但如果只有一个数据库,没有种子脚本怎么办?您可以为每个测试还原备份。我们选择了不同的方法。每个集成测试都在一个从不提交的TransactionScope内运行。这很容易实现。每个测试夹具都从一个具有这些方法(NUnit)的基类继承:
[SetUp]
public void InitTestEnvironment()
{
    SetupTeardown.PerTestSetup();
}

[TearDown]
public void CleanTestEnvironment()
{
    SetupTeardown.PerTestTearDown();
}

SetupTeardown 中:

public static void PerTestSetup()
{
    _tranactionScope = new TransactionScope();
}

public static void PerTestTearDown()
{
    if (_tranactionScope != null)
    {
        _tranactionScope.Dispose(); // Rollback any changes made in a test.
        _tranactionScope = null;
    }
}

其中_tranactionScope是一个静态成员变量。


1
感谢您抽出时间回答这个问题。您概述的测试事务方面确实很有用,现在我也在做这方面的工作。 - tully2003

0

选项2,或者任何运行实际数据库更新脚本的变体都是最好的选择。否则,您不一定会针对与生产环境中相同的数据库进行集成测试(至少在架构方面)。

为了解决您关于每次添加新的DB脚本都需要更新的问题,如果您将所有脚本保存在单个文件夹中,可能在项目内具有“复制(如果较新)”的构建操作,您可以以编程方式读取每个文件并执行其中的脚本。只要您从中读取文件的位置是更新脚本的规范存储库,您就永远不需要再进行任何更改。


虽然我喜欢读取每个文件的想法,这样我就不必更新初始化器,但唯一的问题是通常脚本需要按特定顺序运行。例如,如果脚本1创建了一个表,但脚本2向该表添加了一个字段,并且脚本2先运行,则会失败。不过我相信肯定有解决方法。 - tully2003
将文件名以 ISO 8601 格式的日期时间戳为前缀。 - Josh Gallagher
是的,那是一个明智的做法 - 不确定为什么我没有想到,但谢谢 :) - tully2003

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