Skip to content

NSubstitute

TestingTesting/MockingC#

What it is

NSubstitute is a mocking library for .NET with a deliberately minimal syntax, aiming to read as natural language rather than as configuration.

Create a substitute for an interface, configure returns with Returns, and assert with Received. There is no separate mock object to unwrap.

Installation

dotnet add package NSubstitute

Getting started

The smallest useful thing you can do with it, and what each part means.

Substitutes read as calls
var repo = Substitute.For<IBookRepository>();

// No lambda — call the method and state what it returns.
repo.GetAsync(42, Arg.Any<CancellationToken>())
    .Returns(new Book { Id = 42, Title = "Dune" });

var service = new BookService(repo);   // no .Object needed
var title = await service.GetTitleAsync(42);

Assert.Equal("Dune", title);
await repo.Received(1).GetAsync(42, Arg.Any<CancellationToken>());
await repo.DidNotReceive().DeleteAsync(Arg.Any<int>());
The substitute is the interface, so there is no .Object indirection. Received and DidNotReceive read as assertions rather than verification calls.

Advanced usage

Where the library earns its place over a simpler alternative.

Sequences, callbacks and throwing
// Successive calls return different values.
repo.NextAsync().Returns(first, second, null);

// Compute the return from the arguments.
repo.GetAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
    .Returns(call => new Book { Id = call.Arg<int>() });

// Simulate a failure.
repo.SaveAsync(Arg.Any<Book>()).Returns<Task>(_ => throw new DbException());

// Capture arguments for a detailed assertion.
await repo.Received().SaveAsync(Arg.Is<Book>(b => b.Title == "Dune" && b.Year == 1965));
Arg.Is with a predicate is the most useful matcher: it asserts on the shape of what was passed rather than requiring an exact object match.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Received() call did not match
The message prints the actual calls received. Compare them against your matcher — usually a CancellationToken or a default parameter differs.
Cannot substitute for a non-virtual member
NSubstitute can only intercept interfaces and virtual members. Extract an interface for the type under test.

Best practices

  • Use Arg.Is with a predicate rather than Arg.Any when the argument matters.
  • Assert with Received(n) to pin down call counts, not just that a call happened.
  • Substitute interfaces you own; mocking third-party clients couples tests to their design.
  • Prefer real dependencies via Testcontainers for data access rather than substituting a repository.

Background

Why it exists, and what it was reacting to.

NSubstitute was designed as a reaction to Moq's lambda-heavy setup calls. There is one entry point — Substitute.For<T>() — and configuration reads as an assignment.