nsubstitute / NSubstitute

A friendly substitute for .NET mocking libraries.
https://nsubstitute.github.io
Other
2.65k stars 260 forks source link

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

Open waznico opened 1 year ago

waznico commented 1 year ago

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:

dtchepak commented 1 year ago

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:

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 ```csharp 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 { private Task task; private object value1; private object value2; private object value3; private object value4; private object value5; public AsyncUnaryCall(Task 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 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 MyFunctionAsync(GrpcRequest request, CallOptions options) { return new AsyncUnaryCall(); } } public class Test { [Fact] public void Sample() { var myGrpcClient = Substitute.For(); myGrpcClient.Configure().MyFunctionAsync(Arg.Any(), Arg.Any()).ReturnsForAnyArgs( new AsyncUnaryCall(Task.FromResult(new BoolResponse() { Value = false }), default, default, default, default, default) ); } } ```
waznico commented 1 year ago

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 commented 4 months ago

Is this the same as https://github.com/nsubstitute/NSubstitute/issues/725?