简体   繁体   中英

Strange behaviour in LINQPad with NSubstitute

I wrote the following sample in LINQPad 4 ( v.4.57.02 ) to demonstrate the folly of trying to use NSubstitute ( v1.9.2.0 ) to mock a type and it's non-virtual property:

void Main()
{
    var foo = Substitute.For<Foo>();
    foo.Alarm.Returns(2);    
    foo.Alarm.Dump();
}

public class Foo
{
    public Foo()
    {
        Console.WriteLine("Foo ctor called.");
    }

    public virtual int Alarm
    {
        get; set;
    }
}

This code works as expected and gives the following output:

Foo ctor called.
2

Now, when I edit the code to remove the virtual modifier on the Alarm property, I am expecting to see an NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException exception which includes the wisdom:

If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member. Return values cannot be configured for non-virtual/non-abstract members.

However, the first time I run the modified code I get:

Foo ctor called.
0

And on subsequent runs I get the expected exception.

Now I suspect there's something funny going on with the way that LINQPad manages AppDomains and how NSubstitute's Castle proxies work - but I don't know what. Hands up, I just don't have the time to dig too far into this, and wondered if anyone else has a definitive explanation as it would be comforting to know about the gotchas in the LINQPad execution environment.

If you start open a new instance of LINQPad and run the code without the virtual member it will fail straight away with the expected error.

So here is my guess as to what is happening. The first time the code is run with the virtual member NSubstitute's state looks like this:

var foo = Substitute.For<Foo>();
foo.Alarm         // 1. last call is foo.Alarm
   .Returns(2);   // 2. make foo.Alarm return `2`. Clear last call.
foo.Alarm         // 3. last call is foo.Alarm
   .Dump();       // 4. extension method -- doesn't clear last call

NSubstitute stores the last call made to a substitute statically, so it hangs around until the appdomain goes away. When you modify the code to remove the virtual and run it again, the .Returns(2) in step 2 finds the last call made in step 3 of the previous run, stubs it accordingly, then clears the last call. No further calls are recorded due to the non-virtual member, so subsequent runs fail with the expected error.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM