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.)