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

How to unit-test transactions in 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;
}

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

我找不到使用 InMemory or SQLite 的方法?

我想你问的是提交失败时如何回滚,如果任何语句失败,EF 核心将自动回滚 阅读更多 here , 如果你问其他原因或者你想在回滚发生时做一些事情,只需添加 try catch 块,

    using (var transaction = await 
        _context.Database.BeginTransactionAsync()){
   try { 
                    _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();
                } 
                catch (Exception) 
                { 
                // failed, Do something 
                } }

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

它的外观:

Assert.True(eventList.Contains((int)RelationalEventId.CommittingTransaction));

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

主要的一点是 Entity Framework 记录的事件会根据数据库提供程序而变化,这让我感到惊讶。如果使用 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; }
    }

然后我调整了我的代码,returns 一个内存数据库,这样我就可以按如下方式切换内存数据库提供程序:

    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;
        }
    }

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

        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);