Moq测试Dapper仓储时Mock匿名类型参数及属性过滤问题
问题描述
在基于Dapper实现通用数据仓储、并将其注入业务服务使用的场景下,业务逻辑的LINQ Select操作中传入了前序遍历生成的匿名类型作为仓储方法参数,编写单元测试时无法正确配置Moq规则,实现按匿名参数的属性值过滤返回结果。
相关代码实现
业务服务代码
public class StudyService: IStudyService { private readonly ILogger<StudyService> _logger; private readonly IDataRepository _dataRepository; public StudyService(ILogger<StudyService> logger, IDataRepository dataRepository) { _logger = logger; _dataRepository = dataRepository; } // 待测试方法 public async Task<StudiesResponse> GetAllStudies() { StudiesResponse studies = new StudiesResponse(); List<StudyReponse> list = new List<StudyReponse>(); foreach (StudyDAO study in await _dataRepository.GetEntitiesAsync<StudyDAO>()) { StudyReponse resp = new StudyReponse() { StudyCode = study.StudyCode, Participators = _dataRepository.GetEntitiesAsync<RoleUserDAO>(new { @StudyId = study.Id }).Result.Select(x => new ParticipatorsResponse() { UserEmail = _dataRepository.GetEntityAsync<UserDAO>(new { @Id = x.Id }).Result.Email, RoleName = _dataRepository.GetEntityAsync<RoleDAO>(new { @Id = x.Id }).Result.Name }) }; list.Add(resp); } studies.Studies = list; return studies; } }
注意:原代码中查询User和Role时传入的Id参数存在逻辑错误,用的是关联表RoleUserDAO的主键x.Id,而非外键x.UserId和x.RoleId,这是Mock返回空数据的核心原因之一。
通用Dapper仓储实现
public class DataRepository : IDataRepository { private readonly IConfiguration _configuration; public DataRepository(IConfiguration configuration) { _configuration = configuration; } public async Task<IEnumerable<T>> GetEntitiesAsync<T>() { using(IDbConnection conn = new SqlConnection(_configuration.GetConnectionString("SqlConnection"))) { return await conn.GetListAsync<T>(); } } public async Task<IEnumerable<T>> GetEntitiesAsync<T>(object query) { using (IDbConnection conn = new SqlConnection(_configuration.GetConnectionString("SqlConnection"))) { return await conn.GetListAsync<T>(query); } } public async Task<T> GetEntityAsync<T>(object query) { using (IDbConnection conn = new SqlConnection(_configuration.GetConnectionString("SqlConnection"))) { IEnumerable<T> result = await conn.GetListAsync<T>(query); return result.FirstOrDefault(); } } }
初始测试代码
[TestFixture] public class Tests { IEnumerable<StudyDAO> _studies; IEnumerable<UserDAO> _users; IEnumerable<RoleDAO> _roles; IEnumerable<RoleUserDAO> _roleUsers; [SetUp] public void Setup() { _studies = MockedDAOs.GetMockedStudies(); _users = MockedDAOs.GetMockedUsers(); _roles = MockedDAOs.GetMockedRoles(); _roleUsers = MockedDAOs.GetMockedRoleUsers(); } [Test] public void GetAllStudiesTest() { var _data = new Mock<IDataRepository>(); _data.Setup(m => m.GetEntitiesAsync<StudyDAO>()).Returns(Task.FromResult(_studies)); _data.Setup(m => m.GetEntitiesAsync<RoleUserDAO>(It.IsAny<Object>())).Returns(Task.FromResult(_roleUsers)); _data.Setup(m => m.GetEntityAsync<UserDAO>(It.IsAny<Object>())).Returns(Task.FromResult(_users.Where(u => u.Id ==????))); _data.Setup(m => m.GetEntityAsync<RoleDAO>(It.IsAny<Object>())).Returns(Task.FromResult(_roles.Where(u => u.Id ==????))); var _logger = Mock.Of<ILogger<StudyService>>(); StudyService _service = new StudyService(_logger, _data.Object); var result = _service.GetAllStudies(); Assert.Pass(); } }
Mock测试数据
public static class MockedDAOs { public static IEnumerable<UserDAO> GetMockedUsers() { List<UserDAO> users = new List<UserDAO>(); users.Add(new UserDAO() { Id = 1, Email = "lordvaderr@galacticempire.com", Address = "Alderaan Sector 1234", LanguajePreference = "English", UpdateUser = "Admin" }); users.Add(new UserDAO() { Id = 2, Email = "senatorpalpatine@galacticempire.com", Address = "Alderaan Sector 1233", LanguajePreference = "English", UpdateUser = "Admin" }); users.Add(new UserDAO() { Id = 3, Email = "generalrommohc@galacticempire.com", Address = "Alderaan Sector 1232", LanguajePreference = "English", UpdateUser = "Admin" }); users.Add(new UserDAO() { Id = 4, Email = "secenthsister@galacticempire.com", Address = "Alderaan Sector 1231", LanguajePreference = "English", UpdateUser = "Admin" }); return users; } public static IEnumerable<StudyDAO> GetMockedStudies() { List<StudyDAO> studies = new List<StudyDAO>(); studies.Add(new StudyDAO() { Id = 1, StudyCode = "order66", Company = "Galactic Empire", UpdateUser = "Admin" }); return studies; } public static IEnumerable<RoleDAO> GetMockedRoles() { List<RoleDAO> roles = new List<RoleDAO>(); roles.Add(new RoleDAO() { Id = 1, Name = "General Director", Curriculum = "Ruthless Management I", UpdateUser = "Admin" }); roles.Add(new RoleDAO() { Id = 2, Name = "CEO", Curriculum = "Smoke and Mirrors I", UpdateUser = "Admin" }); roles.Add(new RoleDAO() { Id = 3, Name = "Strategy Director", Curriculum = "Detecting Critical Targets", UpdateUser = "Admin" }); roles.Add(new RoleDAO() { Id = 4, Name = "Deliver", Curriculum = "Silent Aproaches", UpdateUser = "Admin" }); return roles; } public static IEnumerable<RoleUserDAO> GetMockedRoleUsers() { List<RoleUserDAO> roleUsers = new List<RoleUserDAO>(); roleUsers.Add(new RoleUserDAO() { Id = 1, UserId = 1, RoleId = 1, StudyId = 1, UpdateUser = "Admin" }); roleUsers.Add(new RoleUserDAO() { Id = 2, UserId = 2, RoleId = 2, StudyId = 1, UpdateUser = "Admin" }); roleUsers.Add(new RoleUserDAO() { Id = 3, UserId = 3, RoleId = 3, StudyId = 1, UpdateUser = "Admin" }); roleUsers.Add(new RoleUserDAO() { Id = 4, UserId = 4, RoleId = 4, StudyId = 1, UpdateUser = "Admin" }); return roleUsers; } public static StudiesResponse GetMockedStudiesResponse() { List<ParticipatorsResponse> participatorsResponse = new List<ParticipatorsResponse>(); participatorsResponse.Add(new ParticipatorsResponse(){ UserEmail = "lordvaderr@galacticempire.com", RoleName = "General Director" }); participatorsResponse.Add(new ParticipatorsResponse() { UserEmail = "senatorpalpatine@galacticempire.com", RoleName = "CEO" }); participatorsResponse.Add(new ParticipatorsResponse() { UserEmail = "generalrommohc@galacticempire.com", RoleName = "Strategy Director" }); participatorsResponse.Add(new ParticipatorsResponse() { UserEmail = "secenthsister@galacticempire.com", RoleName = "Deliver" }); List<StudyReponse> studyResponse = new List<StudyReponse>(); studyResponse.Add(new StudyReponse() { StudyCode = "order66", Participators = participatorsResponse }); return new StudiesResponse() { Studies = studyResponse }; } }
已尝试的方案与问题
之前尝试通过反射读取匿名类型的属性值,动态过滤Mock数据,代码如下:
[Test] public void GetAllStudiesTest() { var _data = new Mock<IDataRepository>(); _data.Setup(m => m.GetEntitiesAsync<StudyDAO>()).Returns(Task.FromResult(_studies)); _data.Setup(m => m.GetEntitiesAsync<RoleUserDAO>(It.IsAny<Object>())).ReturnsAsync((Object obj) => _roleUsers.Where(x => x.StudyId == (int)obj.GetType().GetProperty("StudyId").GetValue(obj))); _data.Setup(m => m.GetEntityAsync<UserDAO>(It.IsAny<Object>())).ReturnsAsync((Object obj) => _users.Where(x => x.Id == (int)obj.GetType().GetProperty("Id").GetValue(obj)).FirstOrDefault()); _data.Setup(m => m.GetEntityAsync<RoleDAO>(It.IsAny<Object>())).ReturnsAsync((Object obj) => _roles.Where(x => x.Id == (int)obj.GetType().GetProperty("Id").GetValue(obj)).FirstOrDefault()); var _logger = Mock.Of<ILogger<StudyService>>(); StudyService _service = new StudyService(_logger, _data.Object); var result = _service.GetAllStudies(); Assert.True(comparator.Compare(result, MockedDAOs.GetMockedStudiesResponse()).AreEqual); }
运行后发现获取RoleUserDAO列表时返回0条数据,测试不通过。
问题原因与解决方法
核心问题点
- 业务代码参数传错:查询User和Role时,传入的匿名对象用的是
new { @Id = x.Id },这里的x.Id是RoleUser关联表的主键,不是UserId和RoleId,关联查询时字段匹配错误,自然查不到对应数据。 - 匿名类型属性名匹配问题:传入匿名类型时写了
@StudyId、@Id,C#匿名类型生成的属性名会自动去掉开头的@,反射读取时直接读StudyId、Id是对的,但要注意属性值类型匹配,比如StudyId是int类型就不要强转成其他类型。 - 异步方法调用问题:测试方法本身是同步的,调用异步的
GetAllStudies()时没有加.Result或者await,如果测试方法不用async Task修饰,需要显式等待任务完成,否则结果可能还没返回就做断言。
修正后的实现
首先修正业务代码里的参数错误:
// 把原来Select里的x.Id改成对应的外键,去掉匿名类型属性名前不必要的@ Participators = _dataRepository.GetEntitiesAsync<RoleUserDAO>(new { StudyId = study.Id }).Result.Select(x => new ParticipatorsResponse() { UserEmail = _dataRepository.GetEntityAsync<UserDAO>(new { Id = x.UserId }).Result.Email, RoleName = _dataRepository.GetEntityAsync<RoleDAO>(new { Id = x.RoleId }).Result.Name })
然后修正测试代码,给反射加空值判断,同时正确等待异步方法执行:
[Test] public async Task GetAllStudiesTest() { var _data = new Mock<IDataRepository>(); // 配置无参数查询Study _data.Setup(m => m.GetEntitiesAsync<StudyDAO>()) .ReturnsAsync(_studies); // 配置按StudyId查询RoleUser _data.Setup(m => m.GetEntitiesAsync<RoleUserDAO>(It.IsAny<object>())) .ReturnsAsync((object queryParam) => { var studyIdProp = queryParam.GetType().GetProperty("StudyId"); if (studyIdProp == null) return Enumerable.Empty<RoleUserDAO>(); int targetStudyId = (int)studyIdProp.GetValue(queryParam); return _roleUsers.Where(ru => ru.StudyId == targetStudyId); }); // 配置按Id查询User _data.Setup(m => m.GetEntityAsync<UserDAO>(It.IsAny<object>())) .ReturnsAsync((object queryParam) => { var idProp = queryParam.GetType().GetProperty("Id"); if (idProp == null) return null; int targetId = (int)idProp.GetValue(queryParam); return _users.FirstOrDefault(u => u.Id == targetId); }); // 配置按Id查询Role _data.Setup(m => m.GetEntityAsync<RoleDAO>(It.IsAny<object>())) .ReturnsAsync((object queryParam) => { var idProp = queryParam.GetType().GetProperty("Id"); if (idProp == null) return null; int targetId = (int)idProp.GetValue(queryParam); return _roles.FirstOrDefault(r => r.Id == targetId); }); var _logger = Mock.Of<ILogger<StudyService>>(); StudyService _service = new StudyService(_logger, _data.Object); var result = await _service.GetAllStudies(); // 断言验证结果 var expected = MockedDAOs.GetMockedStudiesResponse(); Assert.AreEqual(expected.Studies.Count(), result.Studies.Count()); Assert.AreEqual(expected.Studies.First().StudyCode, result.Studies.First().StudyCode); Assert.AreEqual(4, result.Studies.First().Participators.Count()); }
额外优化建议
- 业务代码里尽量不要直接用
.Result阻塞异步调用,会有死锁风险,把Select里的逻辑改成异步遍历即可,比如先拿到RoleUser列表再循环查询关联数据。 - 反射读取匿名类型属性时可以封装成通用的辅助方法,避免每个Setup都写重复的反射代码。
- 如果觉得反射写起来麻烦,也可以把仓储的查询参数从
object改成强类型的查询对象,比如定义StudyIdQuery、IdQuery类,这样Mock的时候可以直接用It.Is<>匹配参数,不需要反射,代码更健壮。
内容的提问来源于stack exchange,提问作者Luis Agudo
相关产品推荐
相关产品推荐

