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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 05:15:58