如何在 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
失败则事务被回滚,这可能吗?
我想你问的是提交失败时如何回滚,如果任何语句失败,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;执行命令
而如果您将 Sqlite 用于内存数据库,您会得到四个事件:
- 编号:1;已执行命令
- 编号:5;开始交易
- 编号:1;已执行命令
- 编号: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);
我有一个方法可以在事务中做一些工作:
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
失败则事务被回滚,这可能吗?
我想你问的是提交失败时如何回滚,如果任何语句失败,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;执行命令
而如果您将 Sqlite 用于内存数据库,您会得到四个事件:
- 编号:1;已执行命令
- 编号:5;开始交易
- 编号:1;已执行命令
- 编号: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);