You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确Mock DbRawSqlQuery<int>实现存储过程检测方法的单元测试

解决EF中Mock DbRawSqlQuery和SqlQuery时的测试问题

你遇到的问题本质上是因为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:08:12