NSubstitute: Friendly Mocking for .NET Tests
NSubstitute is a .NET mocking library designed to be simple and readable — you set up substitutes with natural syntax instead of verbose lambda chains, making test code easier to write and review.
Key Takeaways
- Substitute.For
() creates a mock of any interface or virtual class with no configuration needed - Returns() sets up return values with natural left-to-right syntax instead of Moq's Setup().Returns() chain
- Arg.Any
() and Arg.Is () provide flexible argument matching without verbose lambda expressions - Received() and DidNotReceive() verify interactions after the fact rather than before the call
- Async substitutes work the same way — Returns(Task.FromResult(value)) or just Returns(value) with async inference
Mocking in .NET has historically meant Moq. It works, but the API produces verbose setup code: mockRepo.Setup(r => r.GetById(It.IsAny<int>())).Returns(user). NSubstitute takes a different approach — it uses the actual method call syntax for setup, which produces shorter and more readable test code. This post covers everything you need to start using NSubstitute: creating substitutes, configuring return values, using argument matchers, verifying calls, handling exceptions, and working with async code.
Installing NSubstitute
dotnet add package NSubstituteNSubstitute targets .NET Standard 2.0 and works with xUnit, NUnit, and MSTest. It requires that the types you substitute are either interfaces or classes with virtual/abstract members — it cannot substitute sealed classes or non-virtual methods (this is a constraint of all .NET mocking libraries that use proxy generation).
Creating Substitutes
using NSubstitute;
public interface IUserRepository
{
User GetById(int id);
IEnumerable<User> GetAll();
void Save(User user);
Task<User> GetByEmailAsync(string email);
}
// Create a substitute for the interface
var repository = Substitute.For<IUserRepository>();
// Multiple interfaces
var service = Substitute.For<IUserService, IDisposable>();
// Abstract class (virtual members only)
var logger = Substitute.For<Logger>();The substitute object is a real instance that satisfies the type constraint. You can pass it to constructors, assign it to variables typed as the interface, and call methods on it without any setup — unset methods return default values (null for reference types, 0 for numerics, false for booleans).
Setting Up Return Values with Returns()
This is where NSubstitute's design shines. Setup reads as: call the method, then say what it returns.
var repository = Substitute.For<IUserRepository>();
// Set up a return value by calling the method first
var user = new User { Id = 1, Name = "Alice" };
repository.GetById(1).Returns(user);
// The next call with the same argument returns the configured value
var result = repository.GetById(1); // returns the Alice user
// Different return values for different arguments
repository.GetById(1).Returns(alice);
repository.GetById(2).Returns(bob);
// Return null explicitly (same as default, but makes intent clear)
repository.GetById(99).Returns((User)null);
// Return a sequence of values — each call returns the next
repository.GetById(1).Returns(alice, bob, null);
// First call: alice, second call: bob, third and beyond: null
// Compute return value from arguments
repository.GetById(Arg.Any<int>()).Returns(callInfo => {
int id = callInfo.Arg<int>();
return new User { Id = id, Name = $"User {id}" };
});Compare this to Moq's equivalent:
// Moq — verbose lambda chain
mockRepo.Setup(r => r.GetById(1)).Returns(alice);
// NSubstitute — call it directly
repository.GetById(1).Returns(alice);Both work; NSubstitute's version requires less visual parsing.
Argument Matchers
Argument matchers let you configure substitutes to respond to a range of inputs rather than a single specific value.
using NSubstitute;
var emailService = Substitute.For<IEmailService>();
// Match any argument of a type
emailService.Send(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
// Match with a predicate
emailService.Send(Arg.Is<string>(s => s.Contains("@")), Arg.Any<string>())
.Returns(true);
emailService.Send(Arg.Is<string>(s => !s.Contains("@")), Arg.Any<string>())
.Returns(false);
// Match specific values
repository.GetById(Arg.Is<int>(id => id > 0)).Returns(validUser);
repository.GetById(Arg.Is<int>(id => id <= 0)).Returns((User)null);
// Mixing matchers and literals — if you use a matcher for one arg,
// use matchers for all args
emailService.SendTo(Arg.Is("admin@example.com"), Arg.Any<string>())
.Returns(true);One important rule: if you use Arg.Any<T>() or Arg.Is<T>() for one parameter, you must use matchers for all parameters. Mixing literals and matchers in the same call will compile but behave unexpectedly.
Verifying Received Calls
NSubstitute uses a "verify after the fact" approach. You make assertions about what was called after the system under test runs, rather than setting up expectations before.
var repository = Substitute.For<IUserRepository>();
var service = new UserService(repository);
// Act
service.CreateUser("Alice", "alice@example.com");
// Assert — verify the repository was called correctly
repository.Received().Save(Arg.Is<User>(u =>
u.Name == "Alice" && u.Email == "alice@example.com"));
// Verify exact number of calls
repository.Received(1).Save(Arg.Any<User>());
// Verify a method was NOT called
repository.DidNotReceive().Delete(Arg.Any<int>());
// Verify any call was received (at least once)
repository.ReceivedWithAnyArgs().Save(default);
// Verify in order (NSubstitute.ReceivedInOrder extension)
Received.InOrder(() => {
repository.GetById(1);
repository.Save(Arg.Any<User>());
});The Received() method works by recording all calls made to the substitute. When you call repository.Received().Save(...), NSubstitute checks its call log against the specified call and argument matchers.
Throwing Exceptions from Substitutes
Testing error handling requires substitutes that throw.
var repository = Substitute.For<IUserRepository>();
// Throw on any call
repository.GetById(Arg.Any<int>())
.Throws(new DatabaseException("Connection lost"));
// Throw for specific arguments
repository.GetById(-1)
.Throws(new ArgumentException("ID must be positive"));
// Throw once, then return normally
repository.GetById(1)
.Returns(
x => throw new DatabaseException("Transient error"),
x => validUser // second call succeeds
);
// For void methods, use a different syntax
repository
.When(r => r.Save(Arg.Any<User>()))
.Throw(new DatabaseException("Disk full"));
// Throw for void methods with argument matching
repository
.When(r => r.Save(Arg.Is<User>(u => u.Email == null)))
.Throw(new ValidationException("Email is required"));The When(...).Throw(...) syntax is needed for void methods because you cannot chain .Throws() onto a method that returns nothing.
Async Substitutes
Async methods are a common source of confusion with mocking libraries. NSubstitute handles them cleanly.
var repository = Substitute.For<IUserRepository>();
// Option 1: Wrap the value in Task.FromResult
repository.GetByEmailAsync("alice@example.com")
.Returns(Task.FromResult(aliceUser));
// Option 2: NSubstitute infers async — simpler
repository.GetByEmailAsync("alice@example.com")
.Returns(aliceUser); // automatically wrapped in Task
// Return null for async methods
repository.GetByEmailAsync("notfound@example.com")
.Returns((User)null);
// Async exceptions
repository.GetByEmailAsync(Arg.Any<string>())
.ThrowsAsync(new DatabaseException("Timeout"));
// Verify async calls
await service.RegisterAsync("alice@example.com", "password");
await repository.Received().GetByEmailAsync("alice@example.com");
await repository.DidNotReceiveWithAnyArgs().Save(default);The simpler Returns(aliceUser) form (without Task.FromResult) works because NSubstitute detects that the return type is Task<User> and wraps the value automatically.
NSubstitute vs Moq: A Practical Comparison
Both libraries are mature and production-ready. The choice comes down to style.
// Moq setup
var mock = new Mock<IUserRepository>();
mock.Setup(r => r.GetById(It.IsAny<int>())).Returns(user);
mock.Setup(r => r.GetByEmailAsync(It.Is<string>(s => s.Contains("@"))))
.ReturnsAsync(user);
mock.Verify(r => r.Save(It.Is<User>(u => u.Name == "Alice")), Times.Once);
var repository = mock.Object; // unwrap the mock to get the interface
// NSubstitute equivalent
var repository = Substitute.For<IUserRepository>();
repository.GetById(Arg.Any<int>()).Returns(user);
repository.GetByEmailAsync(Arg.Is<string>(s => s.Contains("@")))
.Returns(user);
repository.Received(1).Save(Arg.Is<User>(u => u.Name == "Alice"));
// no .Object unwrapping needed — the substitute IS the interfaceKey differences:
- Moq wraps a
Mock<T>object; you must access.Objectto get the interface. NSubstitute returns the interface directly. - Moq's
Setupcomes before the return value; NSubstitute's setup reads left-to-right (call the method, then say what it returns). - Moq uses
Times.Oncefor call count verification; NSubstitute usesReceived(1). - NSubstitute is generally less code per test.
Partial Substitutes
Sometimes you want to substitute only some methods of a class while keeping others real — particularly useful when testing a class against a real partial implementation.
// Partial substitute — real methods run unless configured
var calculator = Substitute.ForPartsOf<Calculator>();
// Override a specific method
calculator.Add(2, 3).Returns(10); // override
// calculator.Multiply(2, 3) still calls the real implementation
// Use with caution on concrete classes — the constructor runs
// and unsubstituted virtual methods use real implementationsPartial substitutes are useful for legacy code where refactoring to interfaces is impractical but you still need to isolate specific behavior for testing.
Configuring Substitutes to Raise Events
var stockService = Substitute.For<IStockService>();
// Raise an event from a substitute
stockService.PriceChanged += Raise.EventWith(new PriceChangedEventArgs
{
Symbol = "AAPL",
NewPrice = 187.50m
});
// The code under test that subscribes to PriceChanged will receive this eventCommon Pitfalls
Forgetting that NSubstitute uses the last call for setup. If you call repository.GetById(1) and repository.GetById(1).Returns(alice) in that order, only the second call configures the substitute. Calling the method in an assertion before the Returns() setup registers it as a real call, not a configuration call. Always set up returns before running the system under test.
Substituting non-virtual class members. If GetById() is not virtual on a concrete class, NSubstitute cannot intercept it. Use interfaces whenever possible.
Mixing argument matchers and literals. repository.GetById(Arg.Any<int>()) works. repository.GetById(1) works. emailService.Send(Arg.Any<string>(), "hello") will produce a warning and may not match as expected — use Arg.Is<string>(s => s == "hello") instead.
Conclusion
NSubstitute's design principle — that test code should read like the production code it replaces — makes a real difference over thousands of tests. Less ceremony means faster test writing, easier PR reviews, and less chance of setting up mocks incorrectly. If you're starting a new .NET project or getting frustrated with Moq's verbosity, NSubstitute is worth the thirty-second install and five-minute migration.