如何在 Entity Framework Core 中对事务进行单元测试?

tom*_*dox 5 sqlite unit-testing transactions in-memory-database entity-framework-core

我有一个在事务中做一些工作的方法:

public async Task<int> AddAsync(Item item)
{
    int result;

    using (var transaction = await _context.Database.BeginTransactionAsync())
    {
        _context.Add(item);

        // Save the item so it has an ItemId
        result = await _context.SaveChangesAsync();

        // perform some actions using that new item's ItemId
        _otherRepository.Execute(item.ItemId);

        transaction.Commit();
    }

    return result;
}
Run Code Online (Sandbox Code Playgroud)

我想添加单元测试来检查如果_context.SaveChangesAsync_otherRepository.Execute失败则事务会回滚,这可能吗?

我看不到使用InMemorySQLite来做到这一点的方法吗?

tom*_*dox 7

@Ilya Chumakov 的出色回答使我能够对交易进行单元测试。我们在评论中的讨论暴露了一些有趣的观点,我认为这些观点值得进入答案,这样它们就会更持久、更容易看到:

主要的一点是实体框架记录的事件依赖于数据库提供者的变化,这让我感到惊讶。如果使用 InMemory 提供程序,您只会收到一个事件:

  1. 编号:1;执行命令

然而,如果您使用 Sqlite 作为内存数据库,您会收到四个事件:

  1. 编号:1;执行命令
  2. 编号:5;开始交易
  3. 编号:1;执行命令
  4. 编号:6;提交交易

我没想到记录的事件会根据数据库提供商的不同而改变。

对于任何想要进一步了解这一点的人,我通过更改 Ilya 的日志记录代码来捕获事件详细信息,如下所示:

    public class FakeLogger : ILogger
    {
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
            Func<TState, Exception, string> formatter)
        {
            var record = new LogRecord
            {
                EventId = eventId.Id,
                RelationalEventId = (RelationalEventId) eventId.Id,
                Description = formatter(state, exception)
            };

            Events.Add(record);

        }

        public List<LogRecord> Events { get; set; } = new List<LogRecord>();

        public bool IsEnabled(LogLevel logLevel) => true;

        public IDisposable BeginScope<TState>(TState state) => null;   
    }

    public class LogRecord
    {
        public EventId EventId { get; set; }
        public RelationalEventId RelationalEventId { get; set; }
        public string Description { get; set; }
    }
Run Code Online (Sandbox Code Playgroud)

然后我调整了返回内存数据库的代码,以便我可以按如下方式切换内存数据库提供程序:

    public class InMemoryDatabase
    {
        public FakeLogger EfLogger { get; private set; }

        public MyDbContext GetContextWithData(bool useSqlite = false)
        {
            EfLogger = new FakeLogger();

            var factoryMock = Substitute.For<ILoggerFactory>();
            factoryMock.CreateLogger(Arg.Any<string>()).Returns(EfLogger);

            DbContextOptions<MyDbContext> options;

            if (useSqlite)
            {
                // In-memory database only exists while the connection is open
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                options = new DbContextOptionsBuilder<MyDbContext>()
                    .UseSqlite(connection)
                    .UseLoggerFactory(factoryMock)
                    .Options;
            }
            else
            {
                options = new DbContextOptionsBuilder<MyDbContext>()
                    .UseInMemoryDatabase(Guid.NewGuid().ToString())
                    // don't raise the error warning us that the in memory db doesn't support transactions
                    .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
                    .UseLoggerFactory(factoryMock)
                    .Options;
            }

            var ctx = new MyDbContext(options);

            if (useSqlite)
            {
                ctx.Database.EnsureCreated();                
            }

            // code to populate the context with test data

            ctx.SaveChanges();

            return ctx;
        }
    }
Run Code Online (Sandbox Code Playgroud)

最后,在我的单元测试中,我确保在测试的断言部分之前清除事件日志,以确保我不会由于在测试的安排部分记录的事件而得到误报:

        public async Task Commits_transaction()
        {
            using (var context = _inMemoryDatabase.GetContextWithData(useSqlite: true))
            {

                // Arrange
                // code to set up date for test

                // make sure none of our setup added the event we are testing for
                _inMemoryDatabase.EfLogger.Events.Clear();

                // Act
                // Call the method that has the transaction;

                // Assert
                var result = _inMemoryDatabase.EfLogger.Events
                    .Any(x => x.EventId.Id == (int) RelationalEventId.CommittingTransaction);
Run Code Online (Sandbox Code Playgroud)


Ily*_*kov 2

您可以检查 EF Core 日志中的RelationalEventId.RollingbackTransaction事件类型。我在这里提供了完整的详细信息:

如何跟踪 Entity Framework Core 事件以进行集成测试?

它看起来如何:

Assert.True(eventList.Contains((int)RelationalEventId.CommittingTransaction));
Run Code Online (Sandbox Code Playgroud)