Azure Pipeline中使用字段而非局部变量触发RedundantArgumentMatcherException
NSubstitute 冗余参数匹配器异常原因分析
问题场景
在Azure Pipeline中运行单元测试时出现如下问题:
- 直接使用类字段
rankGenerator(NSubstitute创建的代理对象)时,单元测试失败,报错:NSubstitute.Exceptions.RedundantArgumentMatcherException : Some argument specifications (e.g. Arg.Is, Arg.Any) were left over after the last call. - 将该字段赋值给局部变量后再使用,测试即可正常通过。
失败测试代码示例
public class RiskTests { private readonly IRankGenerator rankGenerator = Substitute.For<IRankGenerator>(); private const string OriginalRisk = "Danger danger"; private const string OriginalRank = "Rank 1"; private Risk sut; public RiskTests() { sut = Risk.Create(CreateTestActivity(), OriginalRisk, OriginalRank); rankGenerator( Arg.Is<string>(x => x == null || !string.IsNullOrEmpty(x)), Arg.Is<string>(x => x == null || !string.IsNullOrEmpty(x))) .GenerateRank().ReturnsForAnyArgs(_ => "000001"); } [Fact] public void Should_Add_Mitigation() { sut.AddMitigation("Do this to be safe", rankGenerator); sut.Mitigations.ShouldNotBeEmpty(); sut.Mitigations.Count.ShouldBe(1); sut.AddMitigation("And this is important", rankGenerator); sut.Mitigations.Count.ShouldBe(2); } }
通过测试代码示例
public class RiskTests { private readonly IRankGenerator rankGenerator = Substitute.For<IRankGenerator>(); private const string OriginalRisk = "Danger danger"; private const string OriginalRank = "Rank 1"; private Risk sut; public RiskTests() { sut = Risk.Create(CreateTestActivity(), OriginalRisk, OriginalRank); rankGenerator( Arg.Is<string>(x => x == null || !string.IsNullOrEmpty(x)), Arg.Is<string>(x => x == null || !string.IsNullOrEmpty(x))) .GenerateRank().ReturnsForAnyArgs(_ => "000001"); } [Fact] public void Should_Add_Mitigation() { var localRankGenerator = rankGenerator; sut.AddMitigation("Do this to be safe", localRankGenerator); sut.Mitigations.ShouldNotBeEmpty(); sut.Mitigations.Count.ShouldBe(1); sut.AddMitigation("And this is important", localRankGenerator); sut.Mitigations.Count.ShouldBe(2); } }
Pipeline配置
- task: DotNetCoreCLI@2 inputs: command: 'test' projects: '**/RigPlan.sln' arguments: '--filter CI!=false' testRunTitle: 'Run .Net Tests'
原因解释
这个问题的核心在于NSubstitute参数匹配器的工作机制:
- NSubstitute会把
Arg.Is这类参数匹配规则存储在**当前线程的本地存储(TLS)**中,用于匹配后续的方法调用。完成一次匹配设置(比如构造函数中的代理调用配置)后,理论上会清理线程上下文中的匹配规则。 - 但直接访问类级别的代理字段时,JIT编译器或NSubstitute的代理拦截逻辑可能触发对字段的额外隐式访问。这种访问会让NSubstitute检测到线程上下文中残留的匹配规则(可能是构造函数配置时的残留,或是编译器优化导致的上下文复用),误认为有未使用的匹配规则,从而抛出
RedundantArgumentMatcherException。 - 当把字段赋值给局部变量后,局部变量的访问是直接的,不会触发额外的代理拦截或上下文检查,仅传递代理对象的引用,因此不会触发NSubstitute的参数匹配器校验逻辑,测试就能正常执行。
内容的提问来源于stack exchange,提问作者Steffen Skaraas
相关产品推荐
相关产品推荐

