nsubstitute / NSubstitute

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

NSubstitute.Exceptions.SubstituteInternalException #784

Open Allann opened 7 months ago

Allann commented 7 months ago

The package itself asked me to raise this issue with the following code:

NSubstitute.Exceptions.SubstituteInternalException
Please report this exception at https://github.com/nsubstitute/NSubstitute/issues: 

CallCollection.Delete - collection doesn't contain the call
   at NSubstitute.Core.CallCollection.Delete(ICall call)
   at NSubstitute.Core.GetCallSpec.<>c__DisplayClass4_0.<FromPendingSpecification>b__1(ICall lastCall)
   at NSubstitute.Core.PendingSpecificationInfo.Handle[T](Func`2 onCallSpec, Func`2 onLastCall)
   at NSubstitute.Core.GetCallSpec.FromPendingSpecification(MatchArgs matchArgs, PendingSpecificationInfo pendingSpecInfo)
   at NSubstitute.Core.ConfigureCall.SetResultForLastCall(IReturn valueToReturn, MatchArgs matchArgs, PendingSpecificationInfo pendingSpecInfo)
   at NSubstitute.Core.CallRouter.LastCallShouldReturn(IReturn returnValue, MatchArgs matchArgs, PendingSpecificationInfo pendingSpecInfo)
   at NSubstitute.Core.ThreadLocalContext.LastCallShouldReturn(IReturn value, MatchArgs matchArgs)
   at NSubstitute.SubstituteExtensions.ConfigureReturn[T](MatchArgs matchArgs, T returnThis, T[] returnThese)
   at NSubstitute.SubstituteExtensions.Returns[T](T value, T returnThis, T[] returnThese)
   at BinInspector.Api.UnitTests.LoginEndpointsTests.Login_ShouldCallCorrectMethodWithCorrectParameters() in F:\Repos\BinInspector\BinInspector.Api.UnitTests\UnitTest1.cs:line 25
   at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass46_0.<<InvokeTestMethodAsync>b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 253
--- End of stack trace from previous location ---
   at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48
   at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90

To Reproduce I had this test:

    [Fact]
    public async Task Login_ShouldCallCorrectMethodWithCorrectParameters()
    {
        // Arrange
        var mockDbConnection = Substitute.For<IDbConnection>();
        var mockDbTransaction = Substitute.For<IDbTransaction>();
        var loginRequest = new LoginRequest("abc", "123");
        var expectedUserDetails = new UserDetails("01", "1", "123", "Tester", "abc");

        mockDbConnection.QueryFirstOrDefaultAsync<UserDetails>(Arg.Any<string>(), Arg.Any<DynamicParameters>())!.Returns(Task.FromResult(expectedUserDetails));

        // Act
        var result = await LoginEndpoints.Login(mockDbConnection, loginRequest);

        // Assert
        await mockDbConnection.Received().QueryFirstOrDefaultAsync<UserDetails>("EXEC [GET_ValidLogin] @EmployeeNumber, @DeviceUUID", Arg.Is<DynamicParameters>(p =>
            p.Get<string>("@EmployeeNumber") == loginRequest.EmployeeNumber &&
            p.Get<string>("@DeviceUUID") == loginRequest.DeviceUuid));
        result.Should().BeEquivalentTo(expectedUserDetails);
    }

The IDbConnection is from Dapper.

the actual code being tested is:

            using (context)
            {
                context.Open();
                var parameters = new DynamicParameters();
                parameters.Add("@EmployeeNumber", request.EmployeeNumber);
                parameters.Add("@DeviceUUID", request.DeviceUuid);

                var userDetails = await context.QueryFirstOrDefaultAsync<UserDetails>("EXEC [GET_ValidLogin] @EmployeeNumber, @DeviceUUID", parameters);
                return userDetails is not null ? TypedResults.Ok(userDetails) : TypedResults.Unauthorized();
            }

Just running a stored proc using Dapper. This is just a unit test to ensure parameters are passed in the right order.

Expected behaviour A clear and concise description of what you expected to happen, compared to what actually happened.

Environment:

Additional context running in VS2022 Version 17.10.0 Preview 1.0

304NotModified commented 6 months ago

I like to check this, but it's a lot of work to create a demo of the posted code.

Could you please try to strip Dapper and provide the needed classes/interfaces?

y0ung3r commented 1 month ago

Hi, @Allann, @304NotModified

It's not a bug. You are trying to create a mock for an extension method. That's not possible.

However, I agree that the message from the exception is incorrect. There are actually two exceptions being called in this example. The first one will be ignored (most likely because the method is asynchronous). And the second exception is the one you received.

If you try something like this:

(await mockDbConnection.QueryFirstOrDefaultAsync<UserDetails>(Arg.Any<string>(), Arg.Any<DynamicParameters>())).Returns(expectedUserDetails);

you'll get the right exception:

NSubstitute.Exceptions.RedundantArgumentMatcherException: Some argument specifications (e.g. Arg.Is, Arg.Any) were left over after the last call.

NSubstitute.Exceptions.RedundantArgumentMatcherException
Some argument specifications (e.g. Arg.Is, Arg.Any) were left over after the last call.

This is often caused by using an argument spec with a call to a member NSubstitute does not handle (such as a non-virtual member or a call to an instance which is not a substitute), or for a purpose other than specifying a call (such as using an arg spec as a return value). For example:

    var sub = Substitute.For<SomeClass>();
    var realType = new MyRealType(sub);
    // INCORRECT, arg spec used on realType, not a substitute:
    realType.SomeMethod(Arg.Any<int>()).Returns(2);
    // INCORRECT, arg spec used as a return value, not to specify a call:
    sub.VirtualMethod(2).Returns(Arg.Any<int>());
    // INCORRECT, arg spec used with a non-virtual method:
    sub.NonVirtualMethod(Arg.Any<int>()).Returns(2);
    // CORRECT, arg spec used to specify virtual call on a substitute:
    sub.VirtualMethod(Arg.Any<int>()).Returns(2);

To fix this make sure you only use argument specifications with calls to substitutes. If your substitute is a class, make sure the member is virtual.

Another possible cause is that the argument spec type does not match the actual argument type, but code compiles due to an implicit cast. For example, Arg.Any<int>() was used, but Arg.Any<double>() was required.

NOTE: the cause of this exception can be in a previously executed test. Use the diagnostics below to see the types of any redundant arg specs, then work out where they are being created.

Diagnostic information:

Remaining (non-bound) argument specifications:
    any DynamicParameters

All argument specifications:
    any String
    any DynamicParameters