nsubstitute / NSubstitute

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

Setting returnvalue to a substitute, previously configured in a AutoFixture.Register() call - throws CouldNotSetReturnDueToNoLastCallException #602

Closed MagnusMikkelsen closed 4 years ago

MagnusMikkelsen commented 4 years ago

Description When setting the return value for a substitute method, where:

the first Returns() method (foo.GiveMeBar().Returns(bar) in the example) throws a NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException.

It is possible to workaround the problem by either

  1. not configuring calls in the Register call block
  2. or by calling the method, that I want to set returnvalue for, before setting the returnvalue.

I have included these workarounds in the reproduction code as comments.

To Reproduce

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using NSubstitute;

namespace ConsoleApp2
{
    public static class Program
    {
        public static void Main()
        {
            var f = new Fixture().Customize(new AutoNSubstituteCustomization() { ConfigureMembers = true });

            // Set default values for IBar
            f.Register<IBarRequest, IBar>(r =>
            {
                // Workaround 1:
                //Comment line below, and error goes away
                r.DefaultTrue.Returns(true);

                return (IBar)r;
            });

            // Create substitutes
            var foo = f.Create<IFoo>();
            var bar = f.Create<IBar>();

            // Doesn't throw:
            foo
                .GiveMeBarFromNumber(Arg.Any<int>())
                .Returns(bar);

            // Workaround 2:
            // Un-comment line below and error goes away
            // foo.GiveMeBar();

            // This Returns call throws CouldNotSetReturnDueToNoLastCallException
            foo
                .GiveMeBar()
                .Returns(bar);
        }
    }

    public interface IFoo
    {
        IBar GiveMeBar();
        IBar GiveMeBarFromNumber(int number);
    }

    public interface IBar
    {
        bool DefaultTrue { get; }
    }

    public interface IBarRequest : IBar { }
}

Expected behaviour I did not expect any exception, because I called Returns() after calling my substitute, and I am not configuring other substitutes within Returns().

Environment:

dtchepak commented 4 years ago

Thanks for the excellent repro case. 👍

Confirmed this behaviour on Mac, dotnetcore 2.0, NSub 4.2.1, AutoFixture 4.11.0.

I'm not sure exactly why this is happening from a quick look, but I think it is related to nested substitute configuration. Setting IBar.DefaultTrue while configuring the IFoo call makes NSubstitute think the call configuration is done while it is actually still configuring another call. (Similar to https://github.com/nsubstitute/NSubstitute/issues/217, https://github.com/nsubstitute/NSubstitute/issues/56.)

For the foo.GiveMeBarFromNumber(Arg.Any<int>()) call, the use of the arg matcher puts the substitute in a different, configuration mode. I need to look into why this fixes the problem, but for now you can try working around the issue by using NSubstitute.Extensions; and calling Configure() to put the substitute into that configuration mode for the GiveMeBar call as well:

            foo.Configure()
                .GiveMeBar()
                .Returns(bar);

Hope this helps get your test working again until I can get a better answer for you.

zvirja commented 4 years ago

Thanks @MagnusMikkelsen for the nicely crafted scenario and @dtchepak for the brilliant analysis you did. The reasoning is exactly as described above. Upon receiving a call, NSubstitute remembers it as a "last" call, so that it could be configured by a subsequent call. In your case, the sequence is following:

This is an architectural issue which is quite hard to solve. That's why we introduced the Configure() method to provide NSubstitute with a hint that you actually don't use value returned from the call, as you are going to configure it. This way AutoFixture will not kick in, Register() callback will be not invoked and last call info will be not rewritten.

It's very unpleasant side-effect on AutoFixture-NSubstitute boundary, but hopefully workaround helps to mitigate it.

MagnusMikkelsen commented 4 years ago

Thanks for the explanation @dtchepak and @zvirja, and thank you for your hard work on this library, it's fantastic 👍

I guess I'll just use the .Configure() workaround.

zvirja commented 4 years ago

@MagnusMikkelsen Thanks! Let us know if you still need our assistance.