单元测试中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
相关产品推荐
相关产品推荐

