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

单元测试:如何伪造Dapper的QueryMultipleAsync方法?

用Fake It Easy伪造Dapper QueryMultipleAsync的解决方案

我之前踩过一模一样的坑!用Fake It Easy伪造Dapper的QueryMultipleAsync确实有点棘手,核心问题在于这个方法是DbConnection的静态扩展方法,而且返回的SqlMapper.GridReader是Dapper内部的类型,直接伪造容易出各种错误。下面分享两种亲测有效的方法,优先推荐第一种,更符合SOLID原则。

方法一:封装Dapper调用到抽象层(推荐)

最稳妥的方式是把Dapper的调用封装到一个自定义的抽象接口里,这样测试时完全不用碰Dapper的内部实现,只需要伪造自己的接口即可。

步骤1:定义抽象接口

创建一个接口,把你需要的Dapper方法(比如QueryMultipleAsync)抽象出来:

public interface IDapperWrapper
{
    Task<SqlMapper.GridReader> QueryMultipleAsync(
        IDbConnection connection,
        string sql,
        object parameters = null,
        IDbTransaction transaction = null,
        int? commandTimeout = null,
        CommandType? commandType = null);
}

步骤2:实现接口

写一个实现类,内部调用Dapper的原生方法:

public class DapperWrapper : IDapperWrapper
{
    public async Task<SqlMapper.GridReader> QueryMultipleAsync(
        IDbConnection connection,
        string sql,
        object parameters = null,
        IDbTransaction transaction = null,
        int? commandTimeout = null,
        CommandType? commandType = null)
    {
        return await connection.QueryMultipleAsync(
            sql, parameters, transaction, commandTimeout, commandType);
    }
}

步骤3:业务类依赖抽象接口

在你的业务逻辑类中,注入IDapperWrapper而不是直接调用Dapper的扩展方法:

public class YourBusinessService
{
    private readonly IDapperWrapper _dapperWrapper;

    public YourBusinessService(IDapperWrapper dapperWrapper)
    {
        _dapperWrapper = dapperWrapper;
    }

    public async Task<(List<User> Users, List<Order> Orders)> GetUsersAndOrdersAsync(int userId)
    {
        using var connection = new SqlConnection("your-connection-string");
        var gridReader = await _dapperWrapper.QueryMultipleAsync(
            connection,
            "SELECT * FROM Users WHERE Id = @UserId; SELECT * FROM Orders WHERE UserId = @UserId;",
            new { UserId = userId });
        
        var users = await gridReader.ReadAsync<User>().ToListAsync();
        var orders = await gridReader.ReadAsync<Order>().ToListAsync();
        
        return (users, orders);
    }
}

步骤4:编写单元测试

现在用Fake It Easy伪造IDapperWrapper和SqlMapper.GridReader,完全隔离Dapper:

[Test]
public async Task GetUsersAndOrdersAsync_ShouldReturnCorrectData()
{
    // 1. 伪造依赖
    var fakeDapperWrapper = A.Fake<IDapperWrapper>();
    var fakeGridReader = A.Fake<SqlMapper.GridReader>();

    // 2. 设置测试数据
    var testUsers = new List<User> { new User { Id = 1, Name = "Test User" } };
    var testOrders = new List<Order> { new Order { Id = 101, UserId = 1, Amount = 99.99m } };

    // 3. 配置伪造对象的行为
    // 当调用QueryMultipleAsync时返回伪造的GridReader
    A.CallTo(() => fakeDapperWrapper.QueryMultipleAsync(
            A<IDbConnection>._,
            "SELECT * FROM Users WHERE Id = @UserId; SELECT * FROM Orders WHERE UserId = @UserId;",
            A<object>.That.Matches(p => ((dynamic)p).UserId == 1),
            null, null, null))
        .Returns(Task.FromResult(fakeGridReader));

    // 配置GridReader的ReadAsync方法返回测试数据
    A.CallTo(() => fakeGridReader.ReadAsync<User>())
        .Returns(Task.FromResult(testUsers.AsEnumerable()));
    A.CallTo(() => fakeGridReader.ReadAsync<Order>())
        .Returns(Task.FromResult(testOrders.AsEnumerable()));

    // 4. 实例化业务类并执行测试方法
    var service = new YourBusinessService(fakeDapperWrapper);
    var result = await service.GetUsersAndOrdersAsync(1);

    // 5. 验证结果和调用
    Assert.That(result.Users, Is.EqualTo(testUsers));
    Assert.That(result.Orders, Is.EqualTo(testOrders));
    A.CallTo(() => fakeDapperWrapper.QueryMultipleAsync(A<IDbConnection>._, A<string>._, A<object>._, null, null, null))
        .MustHaveHappenedOnceExactly();
}

方法二:直接伪造DbConnection和GridReader(不推荐,仅作参考)

如果不想封装,也可以直接伪造DbConnection,但因为QueryMultipleAsync是静态扩展方法,Fake It Easy无法直接拦截,需要利用Dapper的内部实现细节,这种方式耦合度高,不推荐长期使用:

[Test]
public async Task GetUsersAndOrdersAsync_DirectFake_ShouldWork()
{
    var fakeConnection = A.Fake<IDbConnection>();
    var fakeGridReader = A.Fake<SqlMapper.GridReader>();

    // 注意:这里需要依赖Dapper内部调用ExecuteReaderAsync的逻辑,伪造该方法返回假的DataReader
    A.CallTo(() => fakeConnection.ExecuteReaderAsync(
            A<CommandDefinition>._,
            A<CancellationToken>._))
        .Returns(Task.FromResult(A.Fake<IDataReader>()));

    // 后续还需手动关联GridReader的ReadAsync返回值,这种方式容易因Dapper版本更新失效
}

关键注意点

  • 永远依赖抽象而不是具体类:封装Dapper调用不仅让测试更容易,还能让你在未来切换ORM时更轻松。
  • 验证SQL语句和参数:确保测试中配置的SQL和参数和业务代码中的完全一致,否则Fake It Easy不会拦截调用。
  • 处理GridReader的Dispose:因为SqlMapper.GridReader实现了IDisposable,Fake It Easy会自动处理伪造对象的Dispose,不用额外操作。

内容的提问来源于stack exchange,提问作者Simon.A

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 08:34:49