How can I test if my model has changed and requires a migration?

Viewed 253

When developing a dot net app, we make changes to the model regularly and may have our unit tests simply running of an in-memory, on the fly created database.

Everything could be working fine, all great, then we deploy, and suddenly everything stops working because we forgot that we needed to migrate the database.

How can I write a unit test that will fail if the model has changed yet it doesn't have a migration for that change?

2 Answers

Like this

using MyProject.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;

namespace MyProject.Tests.Migrations
{
    public class MissingMigrationsTest : MyProjectTestBase
    {
        /// <summary>
        /// Sometimes we change the model, but forget to create a migration for it (with dotnet ef migrations add)
        /// We do this test to detect a missing migration.
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task ShouldCreateABlankMigration()
        {
            var db = new MyProjectDbContextFactory();

            // Create design-time services
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddEntityFrameworkDesignTimeServices();
            serviceCollection.AddDbContextDesignTimeServices(db);
            var serviceProvider = serviceCollection.BuildServiceProvider();

            // Create a migration
            var migrationsScaffolder = serviceProvider.GetService<IMigrationsScaffolder>();
            Microsoft.EntityFrameworkCore.Migrations.Design.ScaffoldedMigration migration = migrationsScaffolder.ScaffoldMigration("test", "MyProject");

            var rs = Regex.Replace(migration.MigrationCode, @"\s+", string.Empty);
            //crude but, lets just remove all whitespace, and then check it has a blank Up() function
            rs.Contains("Up(MigrationBuildermigrationBuilder){}").ShouldBeTrue();
        }

    }
}

Thanks to : https://docs.microsoft.com/nl-be/ef/core/cli/services#using-services and regex.

It seems the internal APIs have changed, for EFCore 6 I needed this to get @worthy7's code working:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Text.RegularExpressions;

        var context = new NotificationDbContext(new DbContextOptionsBuilder().UseSqlServer("connection string not used").Options);

        var logs = new List<string>();
        string migration = null;

        try
        {
            var currentAssembly = Assembly.GetExecutingAssembly();

            var reportHandler = new OperationReportHandler(
                s => logs.Add($"err:{s}"),
                s => logs.Add($"warn:{s}"),
                s => logs.Add($"info:{s}"),
                s => logs.Add($"debug:{s}"));

            migration = new DesignTimeServicesBuilder(currentAssembly, currentAssembly, new OperationReporter(reportHandler), new string[0])
                .CreateServiceCollection(context)
                .BuildServiceProvider()
                .GetService<IMigrationsScaffolder>()
                .ScaffoldMigration("test", "MyProject")
                .MigrationCode;
        }
        catch (Exception ex)
        {
            throw new Exception($"An error occurred generating migrations, log messages were: {string.Join(", ", logs)}", ex);
        }

        var migrationWithoutWhitespace = Regex.Replace(migration, @"\s+", string.Empty);

        //crude but, lets just remove all whitespace, and then check it has a blank Up() function
        Assert.True(
            migrationWithoutWhitespace.Contains("Up(MigrationBuildermigrationBuilder){}"),
            $"Model changes were found which are not yet reflected in a migration. Proposed migration to add them: {migration}");
Related