使用MOQ与Xunit测试C#中DbContext.Database的Mock问题排查
问题分析与解决方案
一、Arrange部分的错误
尝试Mock扩展方法
SetCommandTimeout和GetCommandTimeout是RelationalDatabaseFacadeExtensions类下的静态扩展方法,Moq仅能Mock实例的虚方法或接口成员,无法直接Setup静态扩展方法——这就是你报错的核心原因。你在Setup中直接调用x.Database.SetCommandTimeout(ts3),本质是调用静态方法,Moq不支持这种操作。错误选择InMemoryDatabase
EF Core的InMemory数据库是为非关系型场景设计的,完全不支持关系数据库特有的API(包括命令超时、复杂FromSqlRaw查询等),所以用它测试依赖这些特性的代码从一开始就走不通。
二、正确的测试实现方案
方案1:Mock底层依赖(无需修改原代码)
核心思路是:扩展方法SetCommandTimeout本质是操作IRelationalConnection的CommandTimeout属性,我们可以MockDatabaseFacade的GetService<T>方法,返回一个可控的IRelationalConnection实例,以此模拟超时的设置与恢复逻辑。
using Moq; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore; // Arrange // 1. Mock IRelationalConnection,控制命令超时属性 var mockConnection = new Mock<IRelationalConnection>(); // 设置默认超时(比如30秒) mockConnection.SetupProperty(c => c.CommandTimeout, 30); // 2. Mock DatabaseFacade,让它返回上面的连接实例 var mockDatabase = new Mock<DatabaseFacade>(Mock.Of<DbContext>()); mockDatabase.Setup(d => d.GetService<IRelationalConnection>()) .Returns(mockConnection.Object); // 3. Mock你的DbContext,返回Mock的DatabaseFacade var mockDbContext = new Mock<OneFpsReportsDbContext>(); mockDbContext.Setup(c => c.Database).Returns(mockDatabase.Object); // 4. Mock DbSet以模拟FromSqlRaw的返回结果(根据你的泛型方法调整) var testData = new List<YourReportEntity> { /* 插入测试数据 */ }; var mockDbSet = new Mock<DbSet<YourReportEntity>>(); mockDbSet.As<IQueryable<YourReportEntity>>() .Setup(m => m.Provider).Returns(testData.AsQueryable().Provider); mockDbSet.As<IQueryable<YourReportEntity>>() .Setup(m => m.Expression).Returns(testData.AsQueryable().Expression); mockDbSet.As<IQueryable<YourReportEntity>>() .Setup(m => m.ElementType).Returns(typeof(YourReportEntity)); mockDbSet.As<IQueryable<YourReportEntity>>() .Setup(m => m.GetEnumerator()).Returns(testData.GetEnumerator()); mockDbContext.Setup(c => c.Set<YourReportEntity>()).Returns(mockDbSet.Object); var reportView = new SchedulerLPOP10ReportDataView(); // Act var result = reportView.GetReportViewData<YourReportEntity>(mockDbContext.Object); // Assert // 验证超时是否被设置为20分钟(1200秒),然后恢复为原超时 mockConnection.VerifySet(c => c.CommandTimeout = 1200, Times.Once); mockConnection.VerifySet(c => c.CommandTimeout = 30, Times.Once); // 验证FromSqlRaw是否被调用(如果需要) mockDbSet.Verify(m => m.FromSqlRaw(It.IsAny<string>(), It.IsAny<object[]>()), Times.Once); // 验证返回结果是否符合预期 Assert.Equal(testData.Count, result.Count());
方案2:使用SQLite内存数据库(适合需要实际执行SQL的场景)
如果你的测试需要实际执行FromSqlRaw查询(验证SQL语法正确性等),可以用SQLite的内存模式——它支持关系数据库的完整特性,包括命令超时。
using Microsoft.EntityFrameworkCore; using Microsoft.Data.Sqlite; // Arrange // 创建SQLite内存连接 var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); // 配置DbContext选项 var options = new DbContextOptionsBuilder<OneFpsReportsDbContext>() .UseSqlite(connection) .Options; // 创建测试用DbContext并初始化表结构 using var dbContext = new OneFpsReportsDbContext(options); dbContext.Database.EnsureCreated(); // 插入测试数据(如果需要) // dbContext.YourReportTable.AddRange(testData); // dbContext.SaveChanges(); var reportView = new SchedulerLPOP10ReportDataView(); // (可选)用拦截器验证超时设置 var timeoutInterceptor = new CommandTimeoutInterceptor(); options = new DbContextOptionsBuilder<OneFpsReportsDbContext>() .UseSqlite(connection) .AddInterceptors(timeoutInterceptor) .Options; // Act var result = reportView.GetReportViewData<YourReportEntity>(dbContext); // Assert // 验证返回结果 Assert.Equal(expectedCount, result.Count()); // 验证超时是否被正确设置(通过拦截器) Assert.Equal(1200, timeoutInterceptor.SetTimeoutValue);
配套的拦截器实现(用于验证超时)
using Microsoft.EntityFrameworkCore.Diagnostics; using System.Data.Common; public class CommandTimeoutInterceptor : DbCommandInterceptor { public int? SetTimeoutValue { get; private set; } public override InterceptionResult<DbDataReader> ReaderExecuting( DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result) { SetTimeoutValue = command.CommandTimeout; return base.ReaderExecuting(command, eventData, result); } }
内容的提问来源于stack exchange,提问作者Abhishek_Singh_Rana
相关产品推荐
相关产品推荐

