Entity Framework Core 唯一索引测试

4

我有一个模型类:

public class Work
{
    public long Id { get; set; }

    [Required]
    public string Name { get; set; }
}

我希望这个Work.Name是唯一的,所以我定义了DbContext
public class MyDbContext : DbContext
{
    public MyDbContext () : base() { }
    public MyDbContext (DbContextOptions<MyDbContext > options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Work>(entity =>
            entity.HasIndex(e => e.Name).IsUnique()
        );
    }
    public DbSet<Work> Works { get; set; }
}

我想要测试这个功能,所以我有一个类似于这样的测试:

[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
    var work = new Work
    {
        Name = "Test Work"
    };

    using (var context = new MyDbContext (options))
    {
        context.Works.Add(work);
        context.SaveChanges();
    }

    using (var context = new MyDbContext (options))
    {
        context.Works.Add(work);
        context.SaveChanges();
    }

    using (var context = new MyDbContext (options))
    {
         Assert.Equal(1, context.Works.Count());
    }
}

option对象包含InMemoryDatabase的设置)

我不知道应该检查什么,但测试在Assert中失败,而不是第二个SaveChanges()中。数据库(context)包含两个具有相同Name的对象。

我查看了所有相关的问题,但没有看到任何人回答我所问的问题。


4
内存提供程序不像SQL Server提供程序那样生成标识键值,这可能导致您的问题。请参阅此线程以获取更多信息:https://github.com/aspnet/EntityFrameworkCore/issues/14646我相信标识键问题将在3.0版本中得到修复。 - camainc
可以选择在集成测试中对真实的数据库进行测试,或者将测试删除并信任唯一约束起作用。 - Spotted
@camainc,那我是不是应该将我的项目更新到ASP.NET Core 3.0(或更高版本),以便在内存数据库中使唯一索引起作用? - Sasuke Uchiha
@SasukeUchiha 那个工作项被标记为“closed-fixed”,所以我认为你可以放心使用EF Core 3。至于将项目更新到ASP.NET Core 3,我不确定是否需要这样做。你需要检查EF Core 3的要求。我知道这很令人困惑。https://devblogs.microsoft.com/dotnet/announcing-ef-core-3-0-and-ef-6-3-general-availability/ - camainc
2个回答

3

正如其他人指出的那样,InMemory数据库提供程序忽略了所有可能的约束。
我的建议是使用带有“内存”功能的Sqlite提供程序,它将为重复的唯一键抛出异常。

public MyDbContext CreateSqliteContext()
{
    var connectionString = 
        new SqliteConnectionStringBuilder { DataSource = ":memory:" }.ToString();
    var connection = new SqliteConnection(connectionString);
    var options = new DbContextOptionsBuilder<MyDbContext>().UseSqlite(connection);

    return new MyDbContext(options);
}

private void Insert(Work work)
{
    using (var context = CreateSqliteContext())
    {
        context.Works.Add(work);
        context.SaveChanges();
    }    
}

[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
    var work1 = new Work { Name = "Test Work" };
    var work2 = new Work { Name = "Test Work" };

    Insert(work1);

    Action saveDuplicate = () => Insert(work2);

    saveDuplicate.Should().Throw<DbUpdateException>(); // Pass Ok
}

看起来很有前途,但目前我无法在SQLServer或SQLite中实现它。当我成功时,我会将其标记为答案。 - baruchiro

0
测试失败是因为第二个SaveChanges()会从数据库中抛出一个异常,告诉您不能添加另一个具有相同Name的对象。
唯一约束不会悄无声息地执行。相反,尝试添加重复值时,将在尝试执行此操作时引发异常。这样,您就可以实际对其做出反应,而不仅在事后(当您看到您尝试添加的数据不存在时)才注意到它。
您可以使用Assert.Throws进行测试:
[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
    var work = new Work
    {
        Name = "Test Work"
    };

    using (var context = new MyDbContext (options))
    {
        context.Works.Add(work);
        context.SaveChanges();
    }

    using (var context = new MyDbContext (options))
    {
        context.Works.Add(work);

        Assert.Throws<Exception>(() => context.SaveChanges());
    }
}

你还可以在那里指定确切的异常(我不记得是哪个异常被抛出了),并且你还可以将其分配给一个变量(Assert.Throws()返回异常),并验证异常消息以确保这是你期望的确切异常。


2
在我的示例代码中,测试在Assert中失败,而不是在第二个SaveChanges()中失败。SaveChanges中没有异常。 - baruchiro
1
内存数据库提供程序不像 SQL Server 提供程序那样生成标识键值。您必须使用内存来管理自己的键。这个问题将在 3.0 版本中得到修复。 - camainc
@camainc 那么你建议如何对其编写测试? - baruchiro
2
@Baruch 制作一个集成测试,针对真实数据库进行测试。 - poke
1
我会按照@poke的建议去做。这只是一个测试,你只需要运行一次来确保它能够正常工作。在我的解决方案中,我有很多类似的测试 - 快速的一次性测试,以确保某些东西是否正常工作,然后要么注释掉它,要么删除[test]属性,以便自动测试运行器不会挑选它。 - camainc
如果有一个真实的数据库,我认为这是一个端到端测试。 - baruchiro

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