集成测试共享数据库的多个实体框架数据库上下文

本文关键字:数据库 实体 框架 上下文 共享 集成测试 | 更新日期: 2023-09-27 18:25:53

在我的应用程序中,我有多个共享同一数据库的小实体框架dbcontexts,例如:

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。你们怎么想?有更好的方法吗?

集成测试共享数据库的多个实体框架数据库上下文

我经常使用集成测试,因为我仍然认为在涉及数据相关流程时,这是最可靠的测试方式。我还有几个不同的上下文和用于数据库升级的DDL脚本,所以我们的情况非常相似。

我最终得到的是选项4:通过常规用户界面维护单元测试数据库内容。当然,大多数集成测试临时修改数据库内容,作为测试"行动"阶段的一部分(稍后将详细介绍"暂时"),但内容不会在测试会话开始时设置。

原因如下。

在某个阶段,我们还在测试会话开始时通过代码或反序列化XML文件生成数据库内容。(我们还没有EF,但否则我们可能会在数据库初始化器中有一些Seed方法)。渐渐地,我开始对这种做法感到担忧。当数据模型或业务逻辑发生变化时,尤其是当必须设计新的用例时,维护代码/XML是一项艰巨的工作。有时我允许自己对这些测试数据进行轻微的破坏,因为我知道这不会影响测试。

此外,数据必须有意义,因为它们必须与实际应用程序中的数据一样有效和连贯。确保这一点的一种方法是由应用程序本身生成数据,否则不可避免地会在种子方法中复制业务逻辑模拟真实世界的数据实际上非常困难这是我发现的最重要的事情。测试不代表真实用例的数据星座不仅浪费时间,而且是错误的安全性。

因此,我发现自己通过应用程序的前端创建测试数据,然后费力地将这些内容序列化为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;
    }
}

其中CCD_ 4是静态成员变量。

选项2或其任何运行实际DB更新脚本的变体都是最好的。除此之外,您不一定要对生产中的同一数据库进行集成测试(至少在模式方面)。

为了解决每次添加新的DB脚本时都需要更新的问题,如果要将所有脚本保存在一个文件夹中,也许在项目中使用"如果更新,则复制"的构建操作,则可以通过编程读取每个文件并执行其中的脚本。只要从中读取文件的地方是更新脚本的规范存储库,就不需要再进行任何更改。