8

I use NUnit and NSubstitute for unit testing. I have the following:

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() throws AmbiguousArgumentException with the message that it cannot determine argument specifications to use. I have found the following on SO

Cannot determine argument specifications to use

which is related but I cannot apply it in the code above. Why is the above code ambiguous? How else could I specify to Received() that it should accept any argument?

Community
  • 1
  • 1
Drew
  • 169
  • 1
  • 1
  • 11

4 Answers4

20

Since you have several int parameters in the Log method, you have to use the argument specification for each and every one of them.

fakeDataProvider.Received().Log(
    Arg.Any<int>(),
    new DateTime(2000, 1, 1),
    Arg.Is(0),
    Arg.Is(0),
    Arg.Is(0),
    null);
Marcio Rinaldi
  • 3,305
  • 24
  • 23
  • Thanks. I thought that Arg.Any<>() was the culprit because the exception was only thrown when I used it. – Drew Mar 25 '16 at 11:29
  • 1
    It is actually. But the problem doesn't lie in Arg.Any exactly, but the type of the parameter you used the Arg.Any. Since there are other parameters with the same type, NSubstitute doesn't know which one you requested to be Arg.Any, hence you have to specify an argument specification for all parameters with the same type. – Marcio Rinaldi Mar 29 '16 at 14:01
  • @MarcioRinaldi This error message doesn't make sense to me, as the types of method's parameters are defined by the method itself. When there is only one method of that name (which is the most typical case), it should not write this stupid error. I had problems only with null parameter values, because `null` isn't typed, so one must write `Arg.Is((string)null)`. – Al Kepp Mar 21 '18 at 11:10
  • 2
    The argument specification works by creating a list with all the specifications in the order they are called, then it infers which specification relates to which argument by the type and order called. If you have two arguments with the same type, but just one uses the specification, you can't tell for sure which one you wanted to have the specification for. It has no information on for which argument it was used. Maybe the exception could be better worded, but it's unavoidable. By the way, you should be able to use just `null` instead of `Arg.Is((string)null)`. – Marcio Rinaldi Mar 22 '18 at 17:28
0

Well, another trap for young players: if you have several calls to the same method in a single test, do not reuse argument specifiers across calls:

I have a virtual method that takes two 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);

You still get the "Please use specifications for all arguments of the same type".

Solution is to instantiate each time the arguments specifiers:

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);
Spikegee
  • 56
  • 5
0

You can use below code to do it

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

I've experienced the AmbiguousArgumentsException as well, but this was due to a brain-fart, I used the Arg.Any<bool>() as a parameter to a non-substituted instance method. This leaves the argument spec in a queue somewhere, and that will bork the next test that uses NSubstitute.

I've written this Attribute (for NUnit, but it should generalize to other frameworks) to verify proper usage of Arg specs:

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);
    }
}
Remco Schoeman
  • 515
  • 7
  • 12