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

Moq单元测试以自定义类为GroupBy键分组失效的解决方法

问题背景

现有业务实现代码如下:

var data = context.GetAll<UserRole>()
   .GroupBy(x => new RoleGroup { RoleId = x.RoleId, AccessEnum = x.AccessEnum })
   .Where(x => x.Count() > 1);

对应单元测试的模拟数据代码如下:

var userRoles = new List<UserRole>
{
    new UserRole { Id = 1, RoleId = 1, AccessEnum = AccessEnum.Full },
    new UserRole { Id = 2, RoleId = 1, AccessEnum = AccessEnum.Full },
    new UserRole { Id = 3, RoleId = 2, AccessEnum = AccessEnum.ReadOnly },
    new UserRole { Id = 4, RoleId = 2, AccessEnum = AccessEnum.Full },
    new UserRole { Id = 5, RoleId = 2, AccessEnum = AccessEnum.ReadOnly }
};
mockContext.Setup(x => x.GetAll<UserRole>()).Returns(userRoles.AsQueryable());

上述代码存在的现象:

  • 系统正式运行连接真实数据库时,GroupBy逻辑可以正常完成分组
  • 执行单元测试时,分组逻辑会逐条返回独立记录,无法正确聚合
  • 如果把GroupBy的键替换为匿名对象,即写成.GroupBy(x => new { x.RoleId, x.AccessEnum }),单元测试的分组功能可以正常生效
  • 由于业务需要将分组结果传入其他方法,必须使用RoleGroup类型作为分组键,需要在保留自定义类作为GroupBy键的前提下修复单元测试分组异常的问题。
问题根因

两种运行场景下GroupBy的执行逻辑存在差异:

  • 正式环境运行时,GroupBy会被EF Core这类ORM翻译成SQL语句在数据库端执行,数据库分组只判断字段值是否匹配,和C#端的对象相等逻辑无关,所以能正常分组。
  • 单元测试时用List.AsQueryable()模拟数据源,GroupBy走的是LINQ to Objects的内存计算逻辑,内存分组判断两个键是否相等,默认依赖对象类型的Equals()和GetHashCode()方法。
  • 自定义的RoleGroup类默认没有重写这两个方法,默认判断逻辑是两个对象的引用地址相同才判定为相等。分组时每条记录都会new一个新的RoleGroup实例,所有实例的引用地址都不相同,所以每条记录都会被判定为独立分组。
  • 匿名类型之所以能正常分组,是因为C#编译器会自动为匿名类型按属性值重写Equals()和GetHashCode(),只要属性值一致就判定为相等,符合分组预期。
解决方案

核心是让RoleGroup类型的相等判断逻辑和匿名类型保持一致,按属性值判断相等,有两种可选实现方式:

方案1:直接为RoleGroup重写相等判断方法(推荐)

直接修改RoleGroup类,重写Equals和GetHashCode方法,修改后所有场景下的相等判断逻辑统一,不会出现测试和生产行为不一致的问题:

public class RoleGroup
{
    public int RoleId { get; set; }
    public AccessEnum AccessEnum { get; set; }

    public override bool Equals(object obj)
    {
        if (obj is not RoleGroup other) return false;
        return RoleId == other.RoleId && AccessEnum == other.AccessEnum;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(RoleId, AccessEnum);
    }
}

修改后原有业务代码不需要做任何调整,单元测试和生产环境的分组逻辑都能正常运行。

方案2:GroupBy时显式传入自定义相等比较器

如果不方便修改RoleGroup类的默认相等逻辑,可以单独实现一个RoleGroup的相等比较器,在调用GroupBy的时候传入:

// 自定义相等比较器
public class RoleGroupEqualityComparer : IEqualityComparer<RoleGroup>
{
    public bool Equals(RoleGroup x, RoleGroup y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;
        return x.RoleId == y.RoleId && x.AccessEnum == y.AccessEnum;
    }

    public int GetHashCode(RoleGroup obj)
    {
        return HashCode.Combine(obj.RoleId, obj.AccessEnum);
    }
}

// 调整业务代码的GroupBy调用,传入比较器
var data = context.GetAll<UserRole>()
   .GroupBy(
        keySelector: x => new RoleGroup { RoleId = x.RoleId, AccessEnum = x.AccessEnum },
        comparer: new RoleGroupEqualityComparer()
    )
   .Where(x => x.Count() > 1);

注意:大部分ORM不支持在GroupBy中传入自定义比较器,会翻译失败,如果用这个方案需要确认正式环境的ORM兼容这个写法,否则还是优先选择方案1。


内容的提问来源于stack exchange,提问作者Ben

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:45:42