如何为ExecuteSqlRawAsync编写NUnit+Moq单元测试?
问题根源
你遇到的System.NotSupportedException是因为ExecuteSqlRawAsync是RelationalDatabaseFacadeExtensions类下的静态扩展方法,而Moq只能Mock实例的虚方法或接口成员,无法直接Mock静态扩展方法,所以直接对_dbContext.Database.ExecuteSqlRawAsync做Setup/验证会报错。
解决方案:封装可Mock的数据库操作接口
最稳妥且符合依赖倒置原则的方式是把数据库操作封装到一个自定义接口中,让业务类依赖这个接口而非直接依赖DbContext,这样就能用Moq轻松Mock接口方法。
步骤1:定义数据库操作接口及实现
// 定义抽象接口 public interface IDatabaseExecutor { Task<int> ExecuteSqlRawAsync(string sql, CancellationToken cancellationToken = default); Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default); } // 基于DbContext的实现类 public class DatabaseExecutor : IDatabaseExecutor { private readonly YourDbContext _dbContext; public DatabaseExecutor(YourDbContext dbContext) { _dbContext = dbContext; } public async Task<int> ExecuteSqlRawAsync(string sql, CancellationToken cancellationToken = default) { return await _dbContext.Database.ExecuteSqlRawAsync(sql, cancellationToken); } public async Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) { return await _dbContext.Database.BeginTransactionAsync(cancellationToken); } }
步骤2:改造业务类
让业务类依赖IDatabaseExecutor而非直接依赖DbContext:
public class DataTransferService { private readonly IDatabaseExecutor _dbExecutor; // 通过构造注入依赖 public DataTransferService(IDatabaseExecutor dbExecutor) { _dbExecutor = dbExecutor; } public async Task TransferFromTempToFormalAsync() { using var transaction = await _dbExecutor.BeginTransactionAsync(); try { // 调用封装后的SQL执行方法 await _dbExecutor.ExecuteSqlRawAsync("INSERT INTO FormalTable SELECT * FROM #TempTable"); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } }
步骤3:编写单元测试
现在可以用Moq轻松MockIDatabaseExecutor,验证事务的提交/回滚逻辑:
[TestFixture] public class DataTransferServiceTests { [Test] public async Task TransferFromTempToFormalAsync_Success_CommitsTransaction() { // Arrange var mockDbExecutor = new Mock<IDatabaseExecutor>(); var mockTransaction = new Mock<IDbContextTransaction>(); mockDbExecutor.Setup(e => e.BeginTransactionAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(mockTransaction.Object); mockDbExecutor.Setup(e => e.ExecuteSqlRawAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())) .ReturnsAsync(5); // 模拟受影响行数 var service = new DataTransferService(mockDbExecutor.Object); // Act await service.TransferFromTempToFormalAsync(); // Assert mockTransaction.Verify(t => t.CommitAsync(It.IsAny<CancellationToken>()), Times.Once); mockTransaction.Verify(t => t.RollbackAsync(It.IsAny<CancellationToken>()), Times.Never); } [Test] public async Task TransferFromTempToFormalAsync_ThrowsException_RollsBackTransaction() { // Arrange var mockDbExecutor = new Mock<IDatabaseExecutor>(); var mockTransaction = new Mock<IDbContextTransaction>(); mockDbExecutor.Setup(e => e.BeginTransactionAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(mockTransaction.Object); mockDbExecutor.Setup(e => e.ExecuteSqlRawAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())) .ThrowsAsync(new InvalidOperationException("SQL执行失败")); var service = new DataTransferService(mockDbExecutor.Object); // Act & Assert var ex = Assert.ThrowsAsync<InvalidOperationException>(() => service.TransferFromTempToFormalAsync()); Assert.That(ex.Message, Is.EqualTo("SQL执行失败")); mockTransaction.Verify(t => t.RollbackAsync(It.IsAny<CancellationToken>()), Times.Once); mockTransaction.Verify(t => t.CommitAsync(It.IsAny<CancellationToken>()), Times.Never); } }
替代方案(不推荐)
如果不想封装接口,也可以尝试MockDbContext.Database的底层依赖(比如RelationalDatabaseFacade的Dependencies属性),但这种方式依赖EF Core的内部实现,版本迭代后可能失效,稳定性远不如封装接口的方案。
内容的提问来源于stack exchange,提问作者jubi
相关产品推荐
相关产品推荐

