NSubstitute
NSubstitute copied to clipboard
Mocking GRPC calls with NSubstitute in unit tests results in NSubstitute.Exceptions.AmbiguousArgumentsException
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
- Setup an GRPC endpoint and function (protobuf, implementation etc.)
- Create a method, that is calling the GRPC function
- 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
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(...)withmyGrpcClient.Configure().MyFunctionAsync(...).ReturnsForAnyArgs(...)(will need to add import forNSubstitute.Extensions). The use ofConfigure()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)
);
}
}
Hi @dtchepak,
thanks for your reply. I'll try it out by the end of the week when I'm back on the topic.
Is this the same as https://github.com/nsubstitute/NSubstitute/issues/725?