实体框架迁移 - 如何创建单元测试以确保迁移模型是最新的?

Posted

技术标签:

【中文标题】实体框架迁移 - 如何创建单元测试以确保迁移模型是最新的?【英文标题】:Entity Framework Migrations - How to create a Unit Test to Ensure Migrations Model is up to Date? 【发布时间】:2013-10-17 13:05:28 【问题描述】:

我正在使用与 TeamCity、NUnit 和 Git 的持续集成。我最近从 FluentMigrator 迁移(请原谅双关语)到实体框架迁移。我这样做主要是为了利用其中的脚手架功能。

但是,有可能在没有首先将更改搭建到迁移中的情况下签入源代码控制的某些更改(想象一下在提交和推送提交之前应用程序没有运行的场景)。我正在使用pre-tested commit workflow,所以我想在预测试中检测到这个问题,而不是等到调用 migrate.exe(这在我的工作流程中为时已晚并破坏了“绿色存储库”)。

我的问题是,如何创建单元/集成测试来检测迁移模型何时与上下文模型不匹配,以便在尝试迁移之前使构建失败?请注意,我希望它与数据库不匹配,并且不希望从测试中访问数据库。

我试过这种方法:

    [Test]
    public void CheckWhetherEntityFrameworkMigrationsContextIsUpToDate()
    
        Assert.DoesNotThrow(() =>
        
            CallDatabase();
        );

    

    private void CallDatabase()
    
        using (var ctx = new MyContext("SERVER=(local);DATABASE=MyDatabase;Integrated Security=True;"))
        
            var tenant = (from t in ctx.Tenant
                          select t).FirstOrDefault();
        
    

但是当有挂起的迁移时,这总是会失败(而不仅仅是在迁移模型与上下文模型不同步的情况下,这就是我所追求的)。

更新

我已在 EntityFramework 项目中为此问题添加了work item。希望他们会考虑添加一种方法来做到这一点。

【问题讨论】:

你在使用 EF Core 吗?如果是,您是否对其进行了等效测试?以下所有答案似乎都是针对前核心 EF 的。 @lonix - 如果我没记错的话,我相信这是针对 EF 6 的。不,我很长时间没有使用 EF,所以没有更新的测试。 我没有注意到日期,对此感到抱歉:) 【参考方案1】:

如果有人觉得这很有用,我会使用以下代码针对测试数据库运行所有迁移来测试迁移:

[TestClass]
public class MigrationsTests

    [TestMethod]
    public void RunAll()
    
        var configuration = new Configuration();
        var migrator = new DbMigrator(configuration);
        // back to 0
        migrator.Update("0");
        // up to current
        migrator.Update();
        // back to 0
        migrator.Update("0");
    

这会测试所有 UpDown 迁移,并在关闭自动迁移时检测挂起的更改。

注意:确保您针对测试数据库运行此程序。在我的情况下,测试项目的 app.config 具有到测试数据库的连接字符串(只是一个本地 SQLExpress 实例)。

【讨论】:

这不是一个好的测试。例如,当您忘记删除一个字段时,在下一次降级时该表将被删除。那么这个测试无论如何都会成功。您需要每一步向上/向下以确保它们确实有效。 (或者迁移系统应该在升级/降级后实际检查模型是否正确)。 我不会说这不是一个好的测试。它只是上下整个链条,但是,如果你愿意,你可以单独上下。我没有遇到你提到TBH的情况。 为了让它适用于 NUnit,我必须将 [RunInApplicationDomain] 应用到我的测试中。这个属性可以在nuget.org/packages/NUnit.ApplicationDomain找到【参考方案2】:

这里有一些代码将检查是否所有模型更改都已被脚手架到迁移。

bool HasPendingModelChanges()

    // NOTE: Using MigratorScriptingDecorator so changes won't be made to the database
    var migrationsConfiguration = new Migrations.Configuration();
    var migrator = new DbMigrator(migrationsConfiguration);
    var scriptingMigrator = new MigratorScriptingDecorator(migrator);

    try
    
        // NOTE: Using InitialDatabase so history won't be read from the database
        scriptingMigrator.ScriptUpdate(DbMigrator.InitialDatabase, null);
    
    catch (AutomaticMigrationsDisabledException)
    
        return true;
    

    return false;

【讨论】:

这很好用。不过需要注意的是,Configuration 类是 Entity Framework 自动生成的。 谢谢布莱斯!人们知道,这实际上是出于某种原因创建了一个新的数据库连接,如果需要,您可以通过在 Migrations.Configuration 类上设置 TargetDatabase 属性来调整连接。我花了很长时间才弄明白。 从测试项目运行时,您是如何解决'Configuration' is inaccessible due to project level 错误的? @bbodenmiller 设为public 或使用[assembly: InternalsVisibleTo("MyTests")] 属性。【参考方案3】:

我想要到目前为止给出的所有答案中最好的,所以这就是我想出的:

[TestClass()]
public class MigrationsTests

    [TestMethod()]
    public void MigrationsUpDownTest()
    
        // Unit tests don't have a DataDirectory by default to store DB in
        AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory());

        // Drop and recreate database
        BoxContext db = new BoxContext();
        db.Database.Delete();

        var configuration = new Migrations.Configuration();
        var migrator = new DbMigrator(configuration);

        // Retrieve migrations
        List<string> migrations = new List<string>;
        migrations.AddRange(migrator.GetLocalMigrations());

        try
        
            for (int index = 0; index < migrations.Count; index++)
            
                migrator.Update(migrations[index]);
                if (index > 0) 
                    migrator.Update(migrations[index - 1]);
                 else 
                    migrator.Update("0"); //special case to revert initial migration
                
            

            migrator.Update(migrations.Last());
        
        catch (SqlException ex)
        
            Assert.Fail("Should not have any errors when running migrations up and down: " + ex.Errors[0].Message.ToString());
        

        // Optional: delete database
        db.Database.Delete();
    

    [TestMethod()]
    public void PendingModelChangesTest()
    
        // NOTE: Using MigratorScriptingDecorator so changes won't be made to the database
        var migrationsConfiguration = new Migrations.Configuration();
        var migrator = new DbMigrator(migrationsConfiguration);
        var scriptingMigrator = new MigratorScriptingDecorator(migrator);

        try
        
            // NOTE: Using InitialDatabase so history won't be read from the database
            scriptingMigrator.ScriptUpdate(DbMigrator.InitialDatabase, null);
        
        catch (AutomaticMigrationsDisabledException)
        
            Assert.Fail("Should be no pending model changes/migrations should cover all model changes.");
        
    

几个值得注意的事情:

您需要在测试项目中安装实体框架 您需要将[assembly: InternalsVisibleTo("MyTestsProject")] 添加到Configuration 类的顶部 您需要在App.config 中指定一个仅用于测试项目的实体框架连接字符串,因为数据库将被频繁地删除和重新创建 - 如果在构建机器上运行,请记住并行运行可能会导致冲突,因此您可能需要每次构建更改字符串

【讨论】:

【参考方案4】:

我认为这比 Pablo Romeo 的代码更有效。这会升级然后再次降级以捕获降级脚本中丢失的项目。

[TestFixture]
class MigrationTest

    [Test]
    public void RunAll()
    
        var configuration = new Configuration();
        var migrator = new DbMigrator(configuration);

        // Retrieve migrations
        List<string> migrations = new List<string> "0";  // Not sure if "0" is more zero than the first item in list of local migrations
        migrations.AddRange(migrator.GetLocalMigrations());

        migrator.Update(migrations.First());

        // Doe een stapje naar voren, en een stapje terug (www.youtube.com/watch?v=2sg1KAxuWKI)
        // (Dutch pun) meaning: take a small step forward, and a small step back ;)
        for (int index = 0; index < migrations.Count; index++)
        
            migrator.Update(migrations[index]);
            if (index > 0)
                migrator.Update(migrations[index - 1]);
        

        migrator.Update(migrations.Last());

        migrator.Update(migrations.First());
    

【讨论】:

你能把那条评论翻译成英文吗? 这是个玩笑,但它也解释了代码的作用,它说:向前走一步,向后走一步(它是一首荷兰歌曲的名字)。但实际上它前进了两步,后退了一步(第一次迭代除外)。例如:它转到版本 0,然后是版本 1,然后是版本 0,然后是版本 2,然后是版本 1,然后是版本 3,然后是版本 2,然后是版本 4,等等。 这实际上是如何捕捉下部分中丢失的项目的? 是的!因为如果你不删除它,当你再次尝试更新时会出现异常(因为字段/索引已经存在)。【参考方案5】:

EF Core 有一个非常直接的方法:

myDbContext.Database.Migrate();

【讨论】:

这将迁移数据库,而不是告诉您是否应该迁移数据库。这个想法也是在单元测试中检测到这一点。

以上是关于实体框架迁移 - 如何创建单元测试以确保迁移模型是最新的?的主要内容,如果未能解决你的问题,请参考以下文章

如何确保我的 django 应用程序中只有 1 个模型创建迁移?

如何实现 IDbContextFactory 以用于实体框架数据迁移

如何在刚刚创建目标实体模型的 Coredata 中迁移实体

实体框架代码优先 - 初始代码迁移不起作用

在实体框架核心中合并迁移

如何对 Django South“数据迁移”进行单元测试