在测试期间使用伪造的 IDbContextFactory 注入数据进行测试

Testing with fake IDbContextFactory injecting data during tests

提问人:Jimenemex 提问时间:11/17/2023 更新时间:11/17/2023 访问量:31

问:

我正在使用 EFCore 6.0.16 InMemory 数据库提供程序来设置一些测试,但在尝试添加到表后在作用域上下文上执行时遇到了空引用问题。context.SaveChanges()

有一个 xUnit 固定装置,用于帮助为每个测试类创建数据库提供程序,其中包含使用 .InMemoryDatabase

public class FakeDbContextFactory : IDbContextFactory<MyContext>
{
    private readonly DbContextOptions<MyContext> _options;

    public FakeDbContextFactory(string name = "FakeTests")
    {
        _options = new DbContextOptionsBuilder<IPSContext>()
            .UseInMemoryDatabase(name)
            .Options;
    }

    public MyContext CreateDbContext()
    {
        return new MyContext(_options);
    }

    public Task<MyContext> CreateDbContextAsync()
    {
        return Task.FromResult(new MyContext(_options));
    }
}

public class MyFixture : IDisposable
{
    public MyFixture()
    {
    }

    public IDbContextFactory<MyContext> CreateFakeDbContext(string databaseName)
    {
        return new FakeDbContextFactory(databaseName);
    }
}

我的测试对 xml 进行签名,返回一个哈希值并将其与写入表中的预定值进行比较,但此示例是该值的简化形式。我想检查签名的哈希值是否等于插入内存提供程序的值是否等于计算值。

public class HelperTests
{
    private readonly MyFixture  _fixture;
    private readonly IDbContextFactory<MyContext> _fakeDbContextFactory;

    public SignatureHelperTests(MyFixture fixture)
    {
        _fixture = fixture;
        _fakeDbContextFactory = _fixture.CreateFakeDbContext(nameof(HelperTests));
    }

    [Fact]
    public void Test_1()
    {
        // Arrange
        var table = new MyTable
        {
            CreationDateTime = DateTime.Now,
            Id = "1234",
            SignedHash = "HashValue"
        };
    
        using (var context = _fakeDbContextFactory.CreateDbContext())
        {
            context.MyTables.Add(table);
            context.SaveChanges(); // Error is thrown here
   
            // Act
            var sut = new MyHelper(_fakeDbContextFactory);

            var signature = sut.GetValue("<xml>");

            // Assert
            Assert.Equal(context.MyTables.Single().SignedHash, signature.SignedHash);
        }
    }
}

堆栈跟踪如下:

System.NullReferenceException : Object reference not set to an instance of an object.

Stack Trace: 
You may need to build and run this test to see links to source files in the stack trace.
lambda_method304(Closure , String )
<>c__DisplayClass4_0`2.<SanitizeConverter>b__1(Object v)
InMemoryTable`1.SnapshotValue(IProperty property, ValueComparer comparer, IUpdateEntry entry)
InMemoryTable`1.Create(IUpdateEntry entry)
InMemoryStore.ExecuteTransaction(IList`1 entries, IDiagnosticsLogger`1 updateLogger)
InMemoryDatabase.SaveChanges(IList`1 entries)
StateManager.SaveChanges(IList`1 entriesToSave)
StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
<>c.<SaveChanges>b__104_0(DbContext _, ValueTuple`2 t)
NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
DbContext.SaveChanges()

我最初在语句之外有 Act 和 Assert 部分,以弄清楚上下文已被释放,但意识到这无关紧要,因为我的假每次都会返回一个新的上下文,并且每次测试都应该返回一个新的工厂和内存提供程序。using

C# 单元测试 entity-framework-core xunit

评论


答:

0赞 Steve Py 11/17/2023 #1

在 CreateDbContext 中,确保已创建数据库:

public MyContext CreateDbContext()
{
    var context = new MyContext(_options);
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();
    return context;
}

请参见:https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database#inmemory-provider

请注意,内存中数据库可以说是用于单元测试的最糟糕选择。SQLLite 是更好的选择,但仍然可能存在问题。通常,测试的最佳选择是使用存储库模式(而不是通用存储库)作为单元测试的模拟边界。这将模拟存储库调用,而不是 DbContext 或内存中链接的 DbContext。

内存中数据库的问题在于,它不会复制实际数据库的大部分行为,这可能会导致错误或与实际数据库相比不同的结果/行为。SQLLite 的行为确实类似于数据库,但它仍然表示不同的提供程序,其行为与实际选择的数据库提供程序不同。