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

使用NSubstitute模拟Dapper的IDbConnection时遇类型不匹配异常

问题原因分析

Dapper的QueryFirstOrDefaultAsync是扩展方法,并非IDbConnection接口的原生成员。NSubstitute仅能模拟接口/类的原生成员,无法直接配置扩展方法的返回值。你当前的写法会让NSubstitute误将扩展方法的调用解析为对IDbConnection内部成员(如CommandText的setter)的调用,从而抛出类型不匹配异常。

解决方案

推荐两种可行方案,优先选择第一种(封装接口),更简洁易维护:

方案1:封装Dapper操作到自定义接口

1. 定义Dapper封装接口与实现

// 定义封装接口
public interface IDapperWrapper
{
    Task<T> QueryFirstOrDefaultAsync<T>(
        IDbConnection connection, 
        string sql, 
        object param = null, 
        IDbTransaction transaction = null, 
        int? commandTimeout = null, 
        CommandType? commandType = null);
}

// 实现封装类
public class DapperWrapper : IDapperWrapper
{
    public async Task<T> QueryFirstOrDefaultAsync<T>(
        IDbConnection connection, 
        string sql, 
        object param = null, 
        IDbTransaction transaction = null, 
        int? commandTimeout = null, 
        CommandType? commandType = null)
    {
        return await connection.QueryFirstOrDefaultAsync<T>(sql, param, transaction, commandTimeout, commandType);
    }
}

2. 修改控制器代码,注入封装接口

private readonly IDapperContextBuilder _dapperContextBuilder;
private readonly IDapperWrapper _dapperWrapper;

public YourController(IDapperContextBuilder dapperContextBuilder, IDapperWrapper dapperWrapper)
{
    _dapperContextBuilder = dapperContextBuilder;
    _dapperWrapper = dapperWrapper;
}

public async Task UpdateStatus(AuthorityStatusUpdateInputDto input)
{
    using var connection = _dapperContextBuilder.CreateConnection();
    Authority authority = await _dapperWrapper.QueryFirstOrDefaultAsync<Authority>(connection, "SELECT TOP 1 * FROM Authority");

    // 其余代码
}

3. 单元测试中模拟封装接口

[Fact]
public async Task GetAllAuthority()
{
    // Arrange
    var connectionFactory = Substitute.For<IDapperContextBuilder>();
    var fakeConnection = Substitute.For<IDbConnection>();
    connectionFactory.CreateConnection().Returns(fakeConnection);

    var dapperWrapper = Substitute.For<IDapperWrapper>();
    var authority = new MasterData.Authority();
    dapperWrapper.QueryFirstOrDefaultAsync<MasterData.Authority>(Arg.Any<IDbConnection>(), Arg.Any<string>())
                 .ReturnsForAnyArgs(Task.FromResult(authority));

    // 实例化控制器
    var controller = new YourController(connectionFactory, dapperWrapper);

    // Act
    await controller.UpdateStatus(new AuthorityStatusUpdateInputDto());

    // Assert
    // 验证调用逻辑,例如是否正确传入SQL语句
    await dapperWrapper.Received().QueryFirstOrDefaultAsync<MasterData.Authority>(
        Arg.Any<IDbConnection>(), 
        "SELECT TOP 1 * FROM Authority"
    );
}

方案2:模拟Dapper底层依赖(繁琐,不推荐)

通过模拟IDbCommand、IDataReader等底层对象,让Dapper从模拟的读取器中获取数据:

[Fact]
public async Task GetAllAuthority()
{
    // Arrange
    var connectionFactory = Substitute.For<IDapperContextBuilder>();
    var fakeConnection = Substitute.For<IDbConnection>();
    var fakeCommand = Substitute.For<IDbCommand>();
    var fakeReader = Substitute.For<IDataReader>();

    // 模拟Connection创建Command
    fakeConnection.CreateCommand().Returns(fakeCommand);
    // 模拟Command异步执行返回Reader
    fakeCommand.ExecuteReaderAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(fakeReader));

    // 模拟Reader返回数据(需根据Authority的属性配置对应读取逻辑)
    fakeReader.Read().Returns(true, false); // 先返回true表示有数据,再false结束读取
    fakeReader.GetOrdinal("Id").Returns(0);
    fakeReader.GetInt32(0).Returns(1);
    fakeReader.GetOrdinal("Name").Returns(1);
    fakeReader.GetString(1).Returns("Admin");

    connectionFactory.CreateConnection().Returns(fakeConnection);

    // 实例化控制器
    var controller = new YourController(connectionFactory);

    // Act
    await controller.UpdateStatus(new AuthorityStatusUpdateInputDto());

    // Assert
    // 验证CommandText是否正确设置
    fakeCommand.Received().CommandText = "SELECT TOP 1 * FROM Authority";
}

内容的提问来源于stack exchange,提问作者San Jaisy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 21:15:26