NSubstitute 无法确定要使用的参数规范。

9
我使用NUnit和NSubstitute进行单元测试。我有以下内容:
public interface IDataProvider
{
    void Log(int tvmId, DateTime time, int source, int level, int eventCode, string message);
}

...

var fakeDataProvider = Substitute.For<IDataProvider>();
...
fakeDataProvider.Received().Log(
    Arg.Any<int>(),
    new DateTime(2000, 1, 1),
    0,
    0,
    0,
    null);

fakeDataProvider.Received()会抛出AmbiguousArgumentException异常,提示无法确定要使用的参数说明。我在stackoverflow上找到了以下信息:

Cannot determine argument specifications to use

这是相关的,但我无法在上面的代码中应用它。为什么上面的代码是有歧义的?还有其他方法可以指定Received()接受任何参数吗?

4个回答

21

由于您在Log方法中有多个int参数,因此您必须为每个参数使用参数规范。

fakeDataProvider.Received().Log(
    Arg.Any<int>(),
    new DateTime(2000, 1, 1),
    Arg.Is(0),
    Arg.Is(0),
    Arg.Is(0),
    null);

谢谢。我以为 Arg.Any<>() 是罪魁祸首,因为只有在使用它时才会抛出异常。 - Drew
1
实际上是这样的。但问题并不完全在于 Arg.Any,而是你使用了 Arg.Any 的参数类型。由于存在其他相同类型的参数,NSubstitute 不知道你请求哪一个参数为 Arg.Any,因此你必须为所有相同类型的参数指定参数规范。 - Marcio Rinaldi
@MarcioRinaldi 这个错误信息对我来说没有意义,因为方法参数的类型是由方法本身定义的。当只有一个同名方法时(这是最典型的情况),它不应该写出这种愚蠢的错误。我只遇到了空参数值的问题,因为 null 没有类型,所以必须写成 Arg.Is<string>((string)null) - Al Kepp
2
参数规范是通过创建一个包含所有规范的列表并按调用顺序进行排序来工作,然后它通过类型和调用顺序推断哪个规范与哪个参数相关。如果您有两个具有相同类型但只有一个使用规范的参数,则无法确定您希望为哪个参数使用规范。它没有关于使用哪个参数的信息。也许异常可以用更好的措辞,但这是不可避免的。顺便说一下,您应该能够仅使用 null 而不是 Arg.Is<string>((string)null) - Marcio Rinaldi

0

我也遇到过AmbiguousArgumentsException,但这是由于我的疏忽,我将Arg.Any<bool>()用作非替代实例方法的参数。这会将参数规范留在某个队列中,这将破坏使用NSubstitute的下一个测试。

我编写了这个属性(针对NUnit,但它应该适用于其他框架),以验证Arg规范的正确使用:

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);
    }
}

0

又是一个年轻玩家的陷阱:如果在单个测试中有多个对同一方法的调用,请勿跨调用重用参数说明符:

我有一个虚拟方法,它需要两个Dictionary<string, MyStruct>:

var checkArg1 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
var checkArg2 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
mySubtitue.Received(1).myMethod(checkArg1, checkArg2);

// do something that triggers another call to MyMethod 
// ...

// second check using the same argument specifiers
mySubtitue.Received(1).myMethod(checkArg1, checkArg2);

你仍然会得到“请使用相同类型的所有参数规范”的提示。
解决方案是每次实例化参数规范:
var checkArg1 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
var checkArg2 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
mySubtitue.Received(1).myMethod(checkArg1, checkArg2);

// do something that triggers another call to MyMethod 
// ...

// second check using new argument specifiers
checkArg1 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
checkArg2 = Arg.Is<Dictionary<string, MyStruct>>(dico => dico.Count == 0);
mySubtitue.Received(1).myMethod(checkArg1, checkArg2);

0
你可以使用以下代码来实现它
fakeDataProvider.Received().Log(
    Arg.Any<int>(),
    new DateTime(2000, 1, 1),
    Arg.Is(0),
    Arg.Is(0),
    Arg.Is(0),
    null);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接