- Getting started
- Creating a substitute
- Setting a return value
- Return for specific args
- Return for any args
- Return from a function
- Multiple return values
- Replacing return values
- Checking received calls
- Clearing received calls
- Argument matchers
- Callbacks, void calls and When..Do
- Throwing exceptions
- Safe configuration and overlapping calls
- Raising events
- Auto and recursive mocks
- Setting out and ref args
- Actions with argument matchers
- Checking call order
- Partial subs and test spies
- Return for all calls of a type
- Threading
- Compatibility argument matchers
- NSubstitute.Analyzers
- How NSubstitute works
- Search
How NSubstitute works
When we substitute for a class or interface, NSubstitute uses the wonderful Castle DynamicProxy library to generate a new class that inherits from that class or implements that interface. This allows us to use that substitute in place of the original type.
You can think of it working a bit like this:
public class Original {
public virtual int DoStuffWith(string s) => s.Length;
}
// Now if we do:
// var sub = Substitute.For<Original>();
//
// This is a bit like doing:
public class SubstituteForOriginal : Original {
public override int DoStuffWith(string s) {
// Tell NSubstitute to record the call, run when..do actions etc,
// then return the value configured for this call.
handle_call_invocation();
return configured_value_for_call();
}
}
Original sub = new SubstituteForOriginal();
Calamities with classes
For the case when Original
is an interface this works perfectly; every member in the interface will be intercepted by NSubstitute’s logic for recording calls and returning configured values.
There are some caveats when Original
is a class though (hence all the warnings about them in the documentation).
Non-virtual members
If DoStuffWith(string s)
is not virtual
, the SubstituteForOriginal
class will not be able to override it, so when it is called NSubstitute will not know about it. It is effectively invisible to NSubstitute; it can’t record calls to it, it can’t configure values using Returns
, it can’t run actions via When..Do
, it can’t verify the call was received. Instead, the real base implementation of the member will run.
This can cause all sorts of problems if we accidentally attempt to configure a non-virtual call, because NSubstitute will get confused about which call you’re talking about. Usually this will result in a run-time error, but in the worst case it can affect the outcome of your test, or even the following test in the suite, in non-obvious ways. Thankfully we have NSubstitute.Analyzers to detect these cases at compile time.
Internal members
Similar limitations apply to internal virtual
members. Because SubstituteForOriginal
gets generated in a separate assembly, internal
members are invisible to NSubstitute by default. There are two ways to solve this:
- Use
[assembly: InternalsVisibleTo(InternalsVisible.ToDynamicProxyGenAssembly2)]
in the test assembly so that theinternal
member can be overridden. - Make the member
protected internal virtual
so that sub-classes can access the member.
Remember that if the member is non-virtual, NSubstitute will not be able to intercept it regardless of whether it is internal
or InternalsVisibleTo
has been added.
The good news is that NSubstitute.Analyzers will also detect attempts to use internal
members at compile time, and will suggest fixes for these cases.
Real code
The final thing to notice here is that there is the potential for real logic from the Original
class to execute. We’ve already seen how this is possible for non-virtual members, but it can also happen if Original
has code in its constructor. If the constructor calls FileSystem.DeleteAllMyStuff()
, then constructing SubstituteForOriginal
will also run this when the base constructor gets called.
Class conclusion
- Be careful substituting for classes!
- Where possible use interfaces instead.
- Remember NSubstitute works by inheriting from (or implementing) your original type. If you can’t override a member by manually writing a sub-class, then NSubstitute won’t be able to either!
- Install NSubstitute.Analyzers where ever you install NSubstitute. This will help you avoid these (and other) pitfalls.