如何正确Mock DbRawSqlQuery<int>实现存储过程检测方法的单元测试
你遇到的问题本质上是因为LINQ的Single()是扩展方法,Moq无法直接Mock扩展方法;另外SqlQuery如果是EF的扩展方法(或是Database类的非虚成员),同样无法直接通过Setup配置。EF的很多类是为运行时设计的,并非为测试Mock场景优化,所以直接Mock这些类会碰到不少限制。下面给你几种可行的解决方案,按推荐程度排序:
方案1:重构代码,抽象数据库查询逻辑(最推荐)
通过引入抽象接口解耦对EF的直接依赖,这样测试时完全不需要Mock EF的复杂类,逻辑更清晰,也更易维护。
步骤1:创建抽象接口
定义一个负责执行原始SQL查询的接口:
public interface IDatabaseSqlQueryExecutor { int ExecuteScalarInt(string sql); string GetDatabaseName(); // 把获取数据库名的逻辑也抽象出来 }
步骤2:实现接口
用你的DbContext实现这个接口:
public class DatabaseSqlQueryExecutor : IDatabaseSqlQueryExecutor { private readonly IDbContextProvider<MyDbContext> _dbContextProvider; public DatabaseSqlQueryExecutor(IDbContextProvider<MyDbContext> dbContextProvider) { _dbContextProvider = dbContextProvider; } public int ExecuteScalarInt(string sql) { return _dbContextProvider.DbContext.Database.SqlQuery<int>(sql).Single(); } public string GetDatabaseName() { return _dbContextProvider.DbContext.Database.Connection.Database; } }
步骤3:修改StoredProcedure类
让它依赖新的抽象接口,而非直接操作DbContext:
public class StoredProcedure : IStoredProcedure { private readonly IDatabaseSqlQueryExecutor _queryExecutor; public StoredProcedure(IDatabaseSqlQueryExecutor queryExecutor) { _queryExecutor = queryExecutor; } public bool ExistsStoredProcedure(string procedureName) { var query = string.Format( "SELECT COUNT(*) FROM [sys].[objects] WHERE [type_desc] = 'SQL_STORED_PROCEDURE' AND [name] = '{0}';", procedureName); var request = _queryExecutor.ExecuteScalarInt(query); return request > 0 ? true : throw new UserFriendlyException( $"Хранимая процедура {procedureName} не найдена в БД {_queryExecutor.GetDatabaseName()}"); } }
步骤4:编写单元测试
现在Mock就变得非常简单了:
public class StoredProcedureTests { private readonly IStoredProcedure _storedProcedure; private readonly Mock<IDatabaseSqlQueryExecutor> _mockQueryExecutor; public StoredProcedureTests() { _mockQueryExecutor = new Mock<IDatabaseSqlQueryExecutor>(); _storedProcedure = new StoredProcedure(_mockQueryExecutor.Object); } [Fact] public void CheckValueIfStoredProcedureExist() { // Arrange const int count = 1; _mockQueryExecutor.Setup(x => x.ExecuteScalarInt(It.IsAny<string>())) .Returns(count); _mockQueryExecutor.Setup(x => x.GetDatabaseName()).Returns("TestDb"); // Act var result = _storedProcedure.ExistsStoredProcedure("TestProc"); // Assert Assert.True(result); } [Fact] public void ThrowsExceptionWhenStoredProcedureDoesNotExist() { // Arrange const int count = 0; _mockQueryExecutor.Setup(x => x.ExecuteScalarInt(It.IsAny<string>())) .Returns(count); _mockQueryExecutor.Setup(x => x.GetDatabaseName()).Returns("TestDb"); // Act & Assert var exception = Assert.Throws<UserFriendlyException>(() => _storedProcedure.ExistsStoredProcedure("MissingProc")); Assert.Contains("не найдена в БД TestDb", exception.Message); } }
这种方案的优势是彻底解耦了EF依赖,测试逻辑清晰,后续更换ORM时只需修改实现类即可。
方案2:使用EF In-Memory数据库(适合集成测试)
如果不想重构代码,可以用EF内存数据库模拟真实环境,但要注意:内存数据库不支持sys.objects这类系统视图,需要调整存储过程存在性的检测逻辑,或模拟替代表。这种方式更适合集成测试而非纯单元测试。
示例代码:
public class StoredProcedureTests { private readonly IStoredProcedure _storedProcedure; private readonly MyDbContext _dbContext; public StoredProcedureTests() { // 创建内存数据库 var options = new DbContextOptionsBuilder<MyDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .Options; _dbContext = new MyDbContext(options); // 模拟sys.objects的替代表 _dbContext.Database.ExecuteSqlRaw(@" CREATE TABLE MockSysObjects ( name NVARCHAR(100), type_desc NVARCHAR(100) )"); _dbContext.Database.ExecuteSqlRaw( "INSERT INTO MockSysObjects VALUES ('TestProc', 'SQL_STORED_PROCEDURE')"); // 配置DbContextProvider var dbContextProvider = new Mock<IDbContextProvider<MyDbContext>>(); dbContextProvider.Setup(x => x.DbContext).Returns(_dbContext); _storedProcedure = new StoredProcedure(dbContextProvider.Object); } [Fact] public void CheckValueIfStoredProcedureExist() { // 注意:需要修改ExistsStoredProcedure里的查询语句,指向MockSysObjects var result = _storedProcedure.ExistsStoredProcedure("TestProc"); Assert.True(result); } }
方案3:Mock IEnumerable接口(临时应急,不推荐)
如果你必须直接MockDbRawSqlQuery<T>,可以利用它实现IEnumerable<T>接口的特性,通过As<IEnumerable<int>>配置枚举器——因为Single()是基于枚举的扩展方法,只要枚举器返回正确元素,Single()就能正常工作。
另外,若SqlQuery是扩展方法,无法直接Setup,需要MockDatabase的虚方法并返回Mock的DbRawSqlQuery:
[Fact] public void CheckValueIfStoredProcedureExist() { const int count = 1; IEnumerable<int> resultList = new List<int> { count }; // Mock DbRawSqlQuery<T> 作为 IEnumerable<T> var mockSqlQuery = new Mock<DbRawSqlQuery<int>>(); mockSqlQuery.As<IEnumerable<int>>() .Setup(x => x.GetEnumerator()) .Returns(resultList.GetEnumerator()); // Mock Database var mockDatabase = new Mock<Database>(); mockDatabase.Setup(x => x.SqlQuery<int>(It.IsAny<string>(), It.IsAny<object[]>())) .Returns(mockSqlQuery.Object); // Mock DbContext var mockDbContext = new Mock<MyDbContext>(); mockDbContext.Setup(x => x.Database).Returns(mockDatabase.Object); // Mock DbContextProvider _mockDbProvider.Setup(x => x.DbContext).Returns(mockDbContext.Object); // Act var result = _storedProcedure.ExistsStoredProcedure("TestProc"); // Assert Assert.True(result); }
这种方式繁琐且脆弱,EF内部实现变更可能导致测试失效,仅适合临时应急。
内容的提问来源于stack exchange,提问作者Change Shell

