6

How does the .Returns<T> (this T value, ... ) extension method work under the hood?

Specifically, how does .Returns know what method it is intended to configure just from the result of executing that method?

Example:

public interface ICalculator { Add(int a, int b); }

// create mock
var calculator = Substitute.For<ICalculator>();

// How does this piece work under the hood?
calculator.Add(1, 2).Returns(3);
coder958452
  • 145
  • 1
  • 6

2 Answers2

6

Whenever a substitute receives a call, it records information about the call, and updates some global state (threadlocal, as Scott pointed out) recording that it was the most recent substitute called.

When .Returns runs, it looks up the last substitute called, then tells the substitute that its last call should be stubbed to return that specific value. (It also removes it from the collection of received calls, so if we run .Received() the stubbed call doesn't get confused for a real one.)

calculator
    .Add(1, 2)   // substitute records Add(1,2) called. Last substitute
                 // set to `calculator`. Returns default `int` in this case.
    .Returns(3)  // Looks up last sub, sets its last call to return 3.

I think this is a reasonable approximation of what happens. To add a little more precision in case you want to look at the code, a substitute is a dynamic proxy which forwards every call to a "call router" which handles all the logic of the substitute (storing calls, configuring calls, adding callbacks etc.). The global state is a SubstitutionContext, which stores the last call router that received a call.

(Repo links are to v4.0.0-rc1 tag. Later versions may change, but the overall idea should remain fairly consistent.)

David Tchepak
  • 9,826
  • 2
  • 56
  • 68
4

I believe that it works by saving a context (called ISubstitutionContext) in thread local storage when a mocked method is called. Then the call to Returns grabs this context and sets appropriate data in the return object.

The actual implementation of the mocked method would (extremely crudely) look something like:

//Dynamically created mock
public int Add(int a, int b)
{
    var context = new SubstitutionContext("Add", ...);

    //CallContext.LogicalSetData or
    //ThreadStatic or
    //ThreadLocal<T> or
    //...

    return 0;
}

//In some extension class
public static ConfiguredCall Returns<T>(this T value, ...)
{
    var context = SubstitutionContext.Current; //Gets from thread local storage
    return context.LastCallShouldReturn(value);
}
Scott Perham
  • 2,410
  • 1
  • 10
  • 20