Mocking with NSubstitute — Fakes, Stubs, and Spies
Use NSubstitute to isolate units under test: creating substitutes, configuring return values, verifying calls, argument matchers, and the mocking anti-patterns that make tests brittle.
Why Mocking
Unit tests test one unit in isolation. A handler that depends on a repository, an email service, and a clock needs those dependencies replaced with controlled substitutes — so you test the handler's logic, not the infrastructure it calls.
Real dependencies in unit tests:
✗ Repository: requires a database — slow, stateful, brittle
✗ Email service: sends real emails — side effects
✗ Clock: depends on system time — non-deterministic
Substitutes:
✓ Repository.GetByIdAsync() returns what YOU specify
✓ Email service records that it was called — no emails sent
✓ Clock always returns the same DateTime — deterministicCreating Substitutes
// NuGet: NSubstitute
using NSubstitute;
// Create a substitute for any interface
var repo = Substitute.For<IPatientRepository>();
var uow = Substitute.For<IUnitOfWork>();
var email = Substitute.For<IEmailService>();
var clock = Substitute.For<IDateTimeProvider>();
// Inject into the class under test
var sut = new CreatePatientHandler(repo, uow, email, clock);Configuring Return Values
// Returns (stub) — configure what the mock returns
repo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Patient?>(null));
// Returns a specific value for specific arguments
var patientId = Guid.NewGuid();
var patient = Patient.Create("John Smith", new DateOnly(1985, 3, 15), "MRN-001").Value;
repo.GetByIdAsync(patientId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Patient?>(patient));
// Returns different values on consecutive calls
repo.ExistsByMRNAsync("MRN-001", Arg.Any<CancellationToken>())
.Returns(false, true); // first call: false, second call: true
// Returns via function (dynamic return)
repo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
var id = callInfo.Arg<Guid>();
return Task.FromResult<Patient?>(
id == Guid.Empty ? null : patient);
});Argument Matchers
// Arg.Any<T>() — match any argument of type T
repo.AddAsync(Arg.Any<Patient>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
// Arg.Is<T>(predicate) — match specific arguments
repo.AddAsync(
Arg.Is<Patient>(p => p.MRN == "MRN-001"),
Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
// Specific value — exact match
repo.GetByIdAsync(patientId, CancellationToken.None)
.Returns(Task.FromResult<Patient?>(patient));Verifying Calls (Spy Behavior)
// Verify a method was called
await repo.Received(1).AddAsync(Arg.Any<Patient>(), Arg.Any<CancellationToken>());
// Verify NOT called
await repo.DidNotReceive().AddAsync(Arg.Any<Patient>(), Arg.Any<CancellationToken>());
// Verify with argument inspection
await repo.Received(1).AddAsync(
Arg.Is<Patient>(p => p.MRN == "MRN-001" && p.Name == "John Smith"),
Arg.Any<CancellationToken>());
// Verify call count
await repo.Received(2).GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());Complete Handler Test Example
public class CreatePatientHandlerTests
{
private readonly IPatientRepository _repo;
private readonly IUnitOfWork _uow;
private readonly CreatePatientHandler _sut;
public CreatePatientHandlerTests()
{
_repo = Substitute.For<IPatientRepository>();
_uow = Substitute.For<IUnitOfWork>();
_sut = new CreatePatientHandler(_repo, _uow);
}
[Fact]
public async Task Handle_new_patient_should_persist_and_return_id()
{
// Arrange — configure substitute behavior
_repo.ExistsByMRNAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(false);
var cmd = new CreatePatientCommand("John Smith", new DateOnly(1985, 3, 15), "MRN-001");
// Act
var result = await _sut.Handle(cmd, CancellationToken.None);
// Assert — result
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBe(Guid.Empty);
// Assert — interactions
await _repo.Received(1).AddAsync(
Arg.Is<Patient>(p => p.MRN == "MRN-001"),
Arg.Any<CancellationToken>());
await _uow.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_duplicate_mrn_should_return_failure_without_saving()
{
// Arrange
_repo.ExistsByMRNAsync("MRN-DUP", Arg.Any<CancellationToken>())
.Returns(true);
var cmd = new CreatePatientCommand("Jane Doe", new DateOnly(1990, 7, 22), "MRN-DUP");
// Act
var result = await _sut.Handle(cmd, CancellationToken.None);
// Assert — failure
result.IsFailure.Should().BeTrue();
result.Error.Code.Should().Be("Patient.MRNAlreadyExists");
// Assert — nothing was persisted
await _repo.DidNotReceive().AddAsync(Arg.Any<Patient>(), Arg.Any<CancellationToken>());
await _uow.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
}
}Simulating Exceptions
// Configure a substitute to throw
repo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new TimeoutException("Database timeout"));
// Test that the handler handles the exception
[Fact]
public async Task Handle_db_timeout_should_return_failure()
{
_repo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new TimeoutException());
var result = await _sut.Handle(new GetPatientQuery(Guid.NewGuid()), CancellationToken.None);
result.IsFailure.Should().BeTrue();
}Capturing Arguments
// Capture what was passed to the substitute
Patient? capturedPatient = null;
await _repo.AddAsync(Arg.Do<Patient>(p => capturedPatient = p), Arg.Any<CancellationToken>());
// Act
await _sut.Handle(cmd, CancellationToken.None);
// Assert on the captured value
capturedPatient.Should().NotBeNull();
capturedPatient!.Name.Should().Be("John Smith");
capturedPatient.IsActive.Should().BeTrue();Mocking Anti-Patterns
Over-specifying:
✗ Verify exact argument values when any would do
✗ Verify order of calls when order does not matter
Result: tests break on refactoring that does not change behavior
Under-specifying:
✗ Only verify happy path — never test that bad paths skip persistence
✗ Use Arg.Any() when a specific value matters to correctness
Mock everything:
✗ Mock value objects, domain entities, simple DTOs
Only mock: infrastructure interfaces, external services, time providers
Never mock: domain logic, value objects — test them directlyRed Flag / Green Answer
Red Flag: "Our unit test configures 15 substitute calls and verifies 10 of them. The test takes 5 minutes to write and breaks every refactoring."
Over-specified tests couple to implementation details, not behavior. If the handler calls
_repo.GetByIdAsynctwice instead of once but the result is the same, the test should not break.
Green Answer:
Verify only the side effects that matter: "was the patient saved?" and "was SaveChanges called?". Do not verify: "was GetById called before ExistsByMRN?" Implementation order is an internal detail.
Key Takeaway
NSubstitute creates substitutes for interfaces:
.Returns()stubs return values,.Received()verifies calls,.DidNotReceive()verifies something was NOT called. UseArg.Any<T>()for flexibility,Arg.Is<T>(predicate)when the argument matters. Verify behavior (was the patient persisted?), not implementation (was method X called before method Y?). Mock only infrastructure — test domain logic directly.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.