Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocking GRPC calls with NSubstitute in unit tests results in NSubstitute.Exceptions.AmbiguousArgumentsException #722

Open
waznico opened this issue Aug 11, 2023 · 3 comments
Labels
bug Reported problem with NSubstitute behaviour

Comments

@waznico
Copy link

waznico commented Aug 11, 2023

Describe the bug
I'm trying to mock my GRPC calls as I did before in Moq. But NSubstitute is throwing an exception. I used the following code to mock it:

myGrpcClient.MyFunctionAsync(Arg.Any<GrpcRequest>(), Arg.Any<CallOptions>()).ReturnsForAnyArgs(
                new AsyncUnaryCall<BoolResponse>(Task.FromResult(new BoolResponse() { Value = false }), default, default,
                    default, default, default));

The exception I receive looks like this:

NSubstitute.Exceptions.AmbiguousArgumentsException
Cannot determine argument specifications to use. Please use specifications for all arguments of the same type.
Method signature:
    AsyncUnaryCall<GrpcRequest, BoolResponse>(Method<GrpcRequest, BoolResponse>, String, CallOptions, GrpcRequest)
Method arguments (possible arg matchers are indicated with '*'):
    AsyncUnaryCall<GrpcRequest, BoolResponse>(Method<GrpcRequest, BoolResponse>, *<null>*, *Grpc.Core.CallOptions*, *<null>*)
All queued specifications:
    any GrpcRequest
    any CallOptions
Matched argument specifications:
    AsyncUnaryCall<GrpcRequest, BoolResponse>(Method<GrpcRequest, BoolResponse>, <null>, ???, ???)

The method has another overload without CallOptions. If I mock this NSubstitute won't throw an exception, but my application will not call the mock.

Here's the code generated by the GRPC plugin:

[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual grpc::AsyncUnaryCall<global::GrpcIdentity.BoolResponse> MyFunctionAsync(global::GrpcIdentity.GrpcRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
      {
        return MyFunctionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
      }
      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual grpc::AsyncUnaryCall<global::GrpcIdentity.BoolResponse> MyFunctionAsync(global::GrpcIdentity.GrpcRequest request, grpc::CallOptions options)
      {
        return CallInvoker.AsyncUnaryCall(__Method_IsInKKORole, null, options, request);
      }

Mocking of the method that contains fewer arguments that also differs from the method with more paremeters doesn't seem to be accepted by NSubstitute.

To Reproduce

  1. Setup an GRPC endpoint and function (protobuf, implementation etc.)
  2. Create a method, that is calling the GRPC function
  3. Create an unit test to test the method and mock the GRPC function

Expected behaviour
It should be possible to mock MyFunctionsAsync that contains fewer and different parameters.

Environment:

  • NSubstitute version: 5.0.0
  • Platform: .NET 6 WebAPI project on Windows 11
@dtchepak
Copy link
Member

Hi @waznico,

I'm not familiar with GRPC but tried with a naive implementation based on the code you shared and was unable to reproduce this. A few things to try:

  • Add NSubstitute.Analyzers to the project and see if it picks up anything.
  • Replace myGrpcClient.MyFunctionAsync(...).ReturnsForAnyArgs(...) with myGrpcClient.Configure().MyFunctionAsync(...).ReturnsForAnyArgs(...) (will need to add import for NSubstitute.Extensions). The use of Configure() will prevent any real code from being executed in the substituted class that could be consuming arg matchers.

If neither of those work would you be able to share a minimal repro I can run?

Here is the code I used for testing:

Sample test
using NSubstitute;
using NSubstitute.Extensions;
using Xunit;

public class GrpcRequest {}
public class CallOptions
{
  private Metadata headers;
  private DateTime? deadline;
  private CancellationToken cancellationToken;

  public CallOptions(Metadata headers, DateTime? deadline, CancellationToken cancellationToken)
  {
    this.headers = headers;
    this.deadline = deadline;
    this.cancellationToken = cancellationToken;
  }
}
public class Metadata {}
public class BoolResponse
{
  public bool Value { get; internal set; }
}
public class AsyncUnaryCall<T>
{
  private Task<T> task;
  private object value1;
  private object value2;
  private object value3;
  private object value4;
  private object value5;

  public AsyncUnaryCall(Task<T> task, object value1, object value2, object value3, object value4, object value5)
  {
    this.task = task;
    this.value1 = value1;
    this.value2 = value2;
    this.value3 = value3;
    this.value4 = value4;
    this.value5 = value5;
  }

  public AsyncUnaryCall() {}
}

public class GrpcClient {
      public virtual AsyncUnaryCall<BoolResponse> MyFunctionAsync(GrpcRequest request, Metadata headers = null, System.DateTime? deadline = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
      {
        return MyFunctionAsync(request, new CallOptions(headers, deadline, cancellationToken));
      }
      public virtual AsyncUnaryCall<BoolResponse> MyFunctionAsync(GrpcRequest request, CallOptions options)
      {
        return new AsyncUnaryCall<BoolResponse>();
      }
}

public class Test {

  [Fact]
  public void Sample() {
    var myGrpcClient = Substitute.For<GrpcClient>();
    myGrpcClient.Configure().MyFunctionAsync(Arg.Any<GrpcRequest>(), Arg.Any<CallOptions>()).ReturnsForAnyArgs(
              new AsyncUnaryCall<BoolResponse>(Task.FromResult(new BoolResponse() { Value = false }), default, default,
                    default, default, default)
    );
  }
}

@waznico
Copy link
Author

waznico commented Aug 16, 2023

Hi @dtchepak,

thanks for your reply. I'll try it out by the end of the week when I'm back on the topic.

@304NotModified
Copy link
Contributor

Is this the same as #725?

@304NotModified 304NotModified added the bug Reported problem with NSubstitute behaviour label Apr 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Reported problem with NSubstitute behaviour
Projects
None yet
Development

No branches or pull requests

3 participants