NSubstitute-TestFixture 1导致TestFixture 2中出现模棱两可的ArgumentsExc

本文关键字:模棱两可 ArgumentsExc 导致 TestFixture NSubstitute-TestFixture | 更新日期: 2023-09-27 18:29:45

我正在使用NUnit和NSubstitute编写C#单元测试。我正在测试一个类,该类将尝试从实现以下接口的配置提供程序中检索对象:

public interface IConfigProvider<T> {
    T GetConfig(int id);
    T GetConfig(string id);
}

正在测试的类只使用GetConfig的int版本,因此在SetUpFixture中,我执行以下操作来设置一个始终返回相同伪对象的模拟配置提供程序:

IConfigProvider<ConfigType> configProvider = Substitute.For<IConfigProvider<ConfigType>>();
configProvider.GetConfig(Arg.Any<int>()).Returns<ConfigType>(new ConfigType(/* args */);

如果TestFixture是唯一一个正在运行的,那么它运行得非常好。然而,在同一组件中的另一个TestFixture中,我检查收到的呼叫,如下所示:

connection.Received(1).SetCallbacks(Arg.Any<Action<Message>>(), Arg.Any<Action<long>>(), Arg.Any<Action<long, Exception>>());

如果这些Received测试在配置提供程序测试之前运行,则配置测试在SetUpFixture中失败,并出现不明确的ArgumentsException:

Here.Be.Namespace.ProfileManagerTests+Setup (TestFixtureSetUp):
SetUp : NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use.
Please use specifications for all arguments of the same type.
at NSubstitute.Core.Arguments.NonParamsArgumentSpecificationFactory.Create(Object argument, IParameterInfo parameterInfo, ISuppliedArgumentSpecifications suppliedArgumentSpecifications)
at System.Linq.Enumerable.<SelectIterator>d__7`2.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at NSubstitute.Core.Arguments.MixedArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos)
at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MatchArgs matchArgs)
at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs)
at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call)
at System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()
at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
at NSubstitute.Routing.Route.Handle(ICall call)
at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation)
at Castle.DynamicProxy.AbstractInvocation.Proceed()
at Castle.Proxies.IConfigProvider`1Proxy.GetConfig(Int32 id)
at Here.Be.Namespace.ProfileManagerTests.Setup.DoSetup()

真正让我困惑的是,即使在测试运行之间,我也能观察到这种效果——如果我使用NUnit GUI单独运行Received测试,然后单独运行配置测试,配置测试就会失败。如果我立即再次运行配置测试,它们就会通过

我尝试过的东西:

  • 添加configProvider.GetConfig(Arg.Any<string>()).Returns...,以防出现过载问题
  • 我已经阅读了关于参数匹配的NSubstitute文档,但我在那里找不到解决方案。如果是必须为方法的int和string版本提供参数匹配器的情况,我不知道如何做到这一点

碰巧的是,我正在使用的测试只会调用值为0或1的GetConfig方法,所以我只能为这两个值提供Returns规范,而根本不使用匹配,但我想了解如何更普遍地解决这个问题。

NSubstitute-TestFixture 1导致TestFixture 2中出现模棱两可的ArgumentsExc

不明确的参数是指NSubstitute将参数与当前使用的调用进行比较,与它拥有的"参数匹配器"堆栈进行比较(每次调用Arg.Blah时,都会向该堆栈添加一个参数匹配器),并且它无法解析哪个参数去了哪里。

通常情况下,这是由像blah(null, null)这样的调用引起的,其中有一个单参数匹配器排队,但也可能是由于在调用配置之外使用参数匹配器或将其作为非虚拟方法的参数而导致堆栈不同步。

1.8.0版本(在您提出问题后发布)略微改进了对后一种情况的检测,因此可能值得一试。

除此之外,我已经遇到过几次这个问题,并使用了以下(痛苦的)方法。

  • 在隔离状态下运行测试并确保其通过
  • 立即计算出什么测试运行(通常可以猜测,但测试日志在这里可以提供帮助),然后只运行这两个测试。确认失败
  • 查找对Arg.xyz的任何调用,这些调用可能会使任一测试中的参数匹配器排队。确保它被用作调用配置的一部分。有时,可以通过注释行或用其他值替换arg匹配器来确定哪个调用有问题
  • 请确保没有对非虚拟方法的调用会混淆NSubstitute

有时问题可能是由于前一个固定装置造成的,因此您可能需要锻炼前一个装置,并在那里进行探索。:(

当我将Microsoft测试运行程序切换到VSTest.Console时,我也出现了类似的错误(在MSTest.exe下运行时没有发生)。

正如David的回答中所建议的那样,错误是由对具有Arg.*参数的非替换方法的调用引起的。Arg.Any被传递给实际的代码方法,这些方法在没有ReturnsReceived相关方法的情况下被调用。

为了扫描我的测试库以查找此类问题,我使用正则表达式搜索来查找Arg.而不是Arg.后面跟着Returns或前面跟着Received 的行

(?=^.*Arg.*$)(?=^((?!Arg.*'.Returns).)*$)^((?!'.Received'(.*Arg.).)*$

它不是防弹过滤器(例如,它不排除多行语句),但它有助于减少要检查的调用次数。

更改测试顺序是有效的。这不是一个很好的答案,但奏效了——一定要试试!

这是我最终使用的正则表达式(在Rider中,对我来说,但它是标准的.Net正则表达式)。它能很好地处理多行语句。我确实出现了一些附加了Returns()调用的假阴性,但它不到使用总数的1%。

它不处理When()/Do()WithAnyArgs变体,但到目前为止,这些变体在我的特定代码库中只是少数使用。

^(?!'s*'.)['w's_'.]+'.(?!DidNotReceive'(')'s*)(?!Received'('d*')'s*)['w_]+'((((Arg.Any<.*?>'('))|(Arg.Is(<.*?>)?'(.*')))[,'s]*)+')(?!'s*'.Return)(?!'s*?'.Throw)

我已经解决了使用程序集级别属性查找错误使用的Arg规范的问题,该属性验证在测试前后没有排队的参数规范挂起。这是针对NUnit的,但这个概念应该推广到其他单元测试框架。

using System;
using System.Runtime.CompilerServices; 
using Tests;
using NSubstitute; // 4.2.2
using NSubstitute.Core;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
// apply this ITestAction to all tests in the assembly
[assembly: VerifyNSubstituteUsage]
namespace Tests;
/// <summary>
/// This attribute can help if you experience <see cref="NSubstitute.Exceptions.AmbiguousArgumentsException"/>s.
/// It will ensure that no NSubstitute argument specifications are left in the queue, before or after a test.
/// This will happen if you pass <c>Arg.Any&lt;T&gt;()</c> (or other argument spec)
/// to an instance that is not generated with <see cref="Substitute"/><c>.</c><see cref="Substitute.For{T}"/>
/// </summary>
/// <remarks>
/// The <see cref="ITestAction.BeforeTest"/> and <see cref="ITestAction.AfterTest"/> will be run for every test and test fixture
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public class VerifyNSubstituteUsageAttribute : Attribute, ITestAction
{
    public ActionTargets Targets => ActionTargets.Suite | ActionTargets.Test;
    public void BeforeTest(ITest test) => AssertNoQueuedArgumentSpecifications(test);
    public void AfterTest(ITest test) => AssertNoQueuedArgumentSpecifications(test);
    private static void AssertNoQueuedArgumentSpecifications(ITest test, [CallerMemberName] string member = null)
    {
        var specs = SubstitutionContext.Current.ThreadContext.DequeueAllArgumentSpecifications();
        if (specs.Count == 0) return;
        var message = $"{member}: Unused queued argument specifications: '{string.Join("', '", specs)}'.'n" +
                      $"Please check {test.FullName} test for usage of Arg.Is(...) or Arg.Any<T>() " +
                      $"with an instance not generated by Substitute.For<T>(...) ";
        Assert.Fail(message);
    }
}