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

单元测试中Mock仓储时如何匹配无属性但带构造参数的类?

问题:如何匹配无属性的Specification类构造参数进行NSubstitute Mock

开发场景

  • 定义了仅含带int类型id参数构造函数、无属性的类:
public sealed class MySpecification : Specification<MyDomainObject>
{
    // This is all it really is, just a class with constructor that takes in parameters
    // No properties
    public MySpecification(int id)
    {
        Query.Where(x => x.Id == id);
    }
}
  • 在Handler的Handle方法中实例化该类并传入仓储方法:
public async Task<MyDomainDto> Handle(MyHandler request, CancellationToken cancellationToken)
{
    var spec = new MySpecification(request.myId);
    var myDomainEntity = await _myRepository.SingleOrDefaultAsync(spec, cancellationToken);

    // bunch of other logics, yada yada
}
  • 单元测试时使用NSubstitute Mock了IRepository<MyDomainObject>,但无法通过Arg.Is<T>()匹配构造参数为1234的MySpecification实例:
public class MyHandlerTests
{
    private readonly IRepository<MyDomainObject> _myRepositoryMock;

    public MyHandlerTests()
    {
        _myRepositoryMock = Substitute.For<IRepository<MyDomainObject>>();
        _myRepositoryMock
            .SingleOrDefaultAsync(Arg.Is<MySpecification>(x => x....??? ), Arg.Any<CancellationToken>())
            .Returns(myObject);
    }
}

需求:设置当传入的MySpecification构造参数为1234时,返回指定的myObject,但该类无属性,无法直接通过Arg.Is<T>()匹配,该如何实现?


解决方案

方案一:添加只读属性暴露构造参数(最推荐)

直接修改MySpecification类,添加一个只读属性存储构造时传入的id,测试时可直接访问该属性匹配:

public sealed class MySpecification : Specification<MyDomainObject>
{
    public int Id { get; } // 添加只读属性

    public MySpecification(int id)
    {
        Id = id; // 赋值
        Query.Where(x => x.Id == id);
    }
}

测试代码中匹配逻辑:

_myRepositoryMock
    .SingleOrDefaultAsync(Arg.Is<MySpecification>(x => x.Id == 1234), Arg.Any<CancellationToken>())
    .Returns(myObject);

优点:代码清晰可维护,符合面向对象设计原则,后续扩展或其他测试场景可复用该属性。

方案二:通过反射读取私有字段(不推荐,依赖实现细节)

若无法修改原类,可通过反射读取编译器自动生成的私有构造参数存储字段:

using System.Reflection;

// 测试中的匹配逻辑
_myRepositoryMock
    .SingleOrDefaultAsync(
        Arg.Is<MySpecification>(x => 
            (int)x.GetType()
                .GetField("<id>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)
                .GetValue(x) == 1234
        ), 
        Arg.Any<CancellationToken>()
    )
    .Returns(myObject);

注意:该方式依赖C#编译器私有字段命名规则,若编译器版本或编译选项变化可能失效,仅适合临时场景。

方案三:验证Specification的查询逻辑(贴合Specification设计)

Specification核心是定义查询条件,可通过验证条件是否匹配目标ID的对象判断:

_myRepositoryMock
    .SingleOrDefaultAsync(
        Arg.Is<MySpecification>(x => 
            // 获取过滤表达式并编译执行,验证是否匹配ID=1234的对象
            x.Query.ToExpression().Compile().Invoke(new MyDomainObject { Id = 1234 })
        ), 
        Arg.Any<CancellationToken>()
    )
    .Returns(myObject);

前提:你的Specification类的Query属性支持获取表达式(如ToExpression()方法,这是多数Specification模式实现的标准功能)。
优点:不依赖类内部状态,而是验证Specification的实际行为,更符合测试本质——验证逻辑而非实现细节。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 01:44:57