单元测试:如何伪造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
相关产品推荐
相关产品推荐

