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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 00:51:16