如何Mock QueryMultipleAsync及对使用该方法的代码做单元测试?
单元测试Dapper QueryMultipleAsync的可行方案
核心结论:必须封装为可Mock的接口
没错,要脱离真实数据库做单元测试,一定要把Dapper的数据库操作抽象成接口。直接在业务代码里写Dapper调用会让测试绑定数据库,没法单独验证业务逻辑。
最佳实践流程
- 抽象数据访问接口:把
QueryMultipleAsync这类Dapper操作定义成接口方法,参数包含SQL语句、查询参数等必要信息。 - 实现数据访问层:在实现类里调用真实的Dapper方法,处理数据库连接。
- 业务层依赖接口:业务类通过构造函数注入这个接口,而非直接实例化数据访问实现类。
- Mock接口做单元测试:用Moq这类框架模拟接口的返回值,模拟多结果集的返回逻辑。
代码示例
1. 定义数据访问接口
public interface IDbDataAccessor { Task<SqlMapper.GridReader> ExecuteQueryMultipleAsync( string sql, object parameters = null, IDbTransaction transaction = null, CancellationToken cancellationToken = default); }
2. 实现数据访问层
public class DbDataAccessor : IDbDataAccessor { private readonly IDbConnection _connection; public DbDataAccessor(IDbConnection connection) { _connection = connection; } public async Task<SqlMapper.GridReader> ExecuteQueryMultipleAsync( string sql, object parameters = null, IDbTransaction transaction = null, CancellationToken cancellationToken = default) { return await _connection.QueryMultipleAsync( sql, param: parameters, transaction: transaction, cancellationToken: cancellationToken); } }
3. 业务逻辑类(依赖接口)
public class DepartmentService { private readonly IDbDataAccessor _dataAccessor; public DepartmentService(IDbDataAccessor dataAccessor) { _dataAccessor = dataAccessor; } public async Task<(List<Employee> Employees, List<Department> Departments)> GetDeptAndEmployeesAsync(int deptId) { var sql = @" SELECT * FROM Employees WHERE DepartmentId = @DeptId; SELECT * FROM Departments WHERE Id = @DeptId;"; using var grid = await _dataAccessor.ExecuteQueryMultipleAsync(sql, new { DeptId = deptId }); var employees = await grid.ReadAsync<Employee>().ToListAsync(); var departments = await grid.ReadAsync<Department>().ToListAsync(); return (employees, departments); } } // 实体类 public class Employee { public int Id { get; set; } public string Name { get; set; } public int DepartmentId { get; set; } } public class Department { public int Id { get; set; } public string Name { get; set; } }
4. 单元测试(用Moq模拟)
[TestClass] public class DepartmentServiceTests { [TestMethod] public async Task GetDeptAndEmployeesAsync_ReturnsExpectedData() { // 准备测试数据 var testEmployees = new List<Employee> { new Employee { Id = 1, Name = "Bob", DepartmentId = 2 } }; var testDept = new List<Department> { new Department { Id = 2, Name = "Engineering" } }; // Mock GridReader var mockGrid = new Mock<SqlMapper.GridReader>(); mockGrid.Setup(g => g.ReadAsync<Employee>(It.IsAny<bool>())) .ReturnsAsync(testEmployees.AsAsyncEnumerable()); mockGrid.Setup(g => g.ReadAsync<Department>(It.IsAny<bool>())) .ReturnsAsync(testDept.AsAsyncEnumerable()); // Mock数据访问接口 var mockAccessor = new Mock<IDbDataAccessor>(); mockAccessor.Setup(a => a.ExecuteQueryMultipleAsync( It.IsAny<string>(), It.Is<object>(p => ((dynamic)p).DeptId == 2), null, default)) .ReturnsAsync(mockGrid.Object); // 初始化业务服务 var service = new DepartmentService(mockAccessor.Object); // 执行测试方法 var result = await service.GetDeptAndEmployeesAsync(2); // 断言结果 Assert.AreEqual(1, result.Employees.Count); Assert.AreEqual("Bob", result.Employees[0].Name); Assert.AreEqual(1, result.Departments.Count); Assert.AreEqual("Engineering", result.Departments[0].Name); // 验证接口调用次数和参数 mockAccessor.Verify(a => a.ExecuteQueryMultipleAsync( It.Is<string>(s => s.Contains("DepartmentId = @DeptId")), It.Is<object>(p => ((dynamic)p).DeptId == 2), null, default), Times.Once); } }
实用提示
- 尽量避免直接Mock
SqlMapper.GridReader,更优雅的方式是在数据访问接口中直接返回封装好的结果(比如Task<(List<Employee>, List<Department>)>),这样Mock起来更简单,业务层也不用处理GridReader的细节。 - 单元测试只关注业务逻辑的正确性:比如参数传递是否正确、结果是否按预期处理。Dapper本身的SQL执行正确性,应该用集成测试(连接真实数据库)来验证。
- 如果业务逻辑依赖多结果集的顺序,Mock时一定要保证返回的顺序和真实场景完全一致,避免测试结果失真。
内容的提问来源于stack exchange,提问作者Antesh Pandey
相关产品推荐
相关产品推荐

