NSubstitute — Simpler Mocking Syntax for the BlogApp

📋 Table of Contents
  1. NSubstitute Syntax
  2. Common Mistakes

NSubstitute offers a cleaner, more ergonomic syntax than Moq for the majority of mocking scenarios. Rather than Moq’s mock.Setup(r => r.Method()).Returns(value) pattern, NSubstitute uses extension methods on the substitute directly: repo.GetByIdAsync(1).Returns(post). For most BlogApp service tests, NSubstitute’s simpler syntax produces more readable tests. The choice between Moq and NSubstitute is largely stylistic — both are mature, actively maintained, and fully capable.

NSubstitute Syntax

// dotnet add package NSubstitute

// ── Creating substitutes ───────────────────────────────────────────────────
IPostsRepository repo = Substitute.For<IPostsRepository>();
ICurrentUserService user = Substitute.For<ICurrentUserService>();

// ── Configure return values ───────────────────────────────────────────────
// Exact argument:
repo.GetByIdAsync(42, Arg.Any<CancellationToken>()).Returns(
    new Post { Id = 42, Title = "Test Post" });

// Any argument:
repo.GetBySlugAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
    .Returns((Post?)null);

// Conditional argument:
repo.GetByIdAsync(
    Arg.Is<int>(id => id > 0),
    Arg.Any<CancellationToken>())
    .Returns(new Post { Id = 1 });

// Property:
user.UserId.Returns("author-123");
user.IsAuthenticated.Returns(true);

// ── Callbacks ─────────────────────────────────────────────────────────────
Post? savedPost = null;
await repo.AddAsync(Arg.Do<Post>(p => savedPost = p), Arg.Any<CancellationToken>());
// Arg.Do captures the argument when the method is called

// ── Sequences — different values on successive calls ─────────────────────
repo.GetViewCountAsync(1, Arg.Any<CancellationToken>())
    .Returns(100, 101, 102);  // first call=100, second=101, third=102

// ── Verify calls ──────────────────────────────────────────────────────────
// Received — was it called?
await repo.Received(1).AddAsync(
    Arg.Is<Post>(p => p.Status == "draft"),
    Arg.Any<CancellationToken>());

// DidNotReceive — was it NOT called?
await repo.DidNotReceive().AddAsync(
    Arg.Any<Post>(), Arg.Any<CancellationToken>());

// ── Complete PostsService test with NSubstitute ───────────────────────────
public class PostsService_NSubstitute_Tests
{
    private readonly IPostsRepository    _repo = Substitute.For<IPostsRepository>();
    private readonly ICurrentUserService _user = Substitute.For<ICurrentUserService>();
    private readonly PostsService        _sut;

    public PostsService_NSubstitute_Tests()
    {
        _user.UserId.Returns("author-123");
        _sut = new PostsService(_repo, _user);
    }

    [Fact]
    public async Task CreateAsync_WithDuplicateSlug_ThrowsConflict()
    {
        _repo.SlugExistsAsync("taken", Arg.Any<CancellationToken>()).Returns(true);

        Func<Task> act = () => _sut.CreateAsync(
            new CreatePostRequest("Title", "taken", "Body", "draft"), default);

        await act.Should().ThrowAsync<ConflictException>();
        await _repo.DidNotReceive().AddAsync(Arg.Any<Post>(), Arg.Any<CancellationToken>());
    }
}

// ── NSubstitute vs Moq syntax comparison ─────────────────────────────────
// MOQ:
// mock.Setup(r => r.GetByIdAsync(1, default)).ReturnsAsync(post);
// mock.Verify(r => r.GetByIdAsync(1, default), Times.Once);

// NSUBSTITUTE (equivalent):
// repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(Task.FromResult(post));
// await repo.Received(1).GetByIdAsync(1, Arg.Any<CancellationToken>());
Note: NSubstitute verifies calls using the same fluent syntax as configuration — await repo.Received(1).GetByIdAsync(42, ...). This reads naturally: “the repository should have received exactly 1 call to GetByIdAsync with argument 42.” The verification is part of the assertion phase and reads like plain English. Moq’s mock.Verify(r => r.GetByIdAsync(42, default), Times.Once) requires a separate API that looks different from the setup syntax, which some developers find less intuitive.
Tip: NSubstitute’s Arg.Do<T>(action) is the equivalent of Moq’s Callback<T>() — it executes a callback when the method is called with a matching argument. Use it to capture passed objects for later assertion: Post? captured = null; repo.AddAsync(Arg.Do<Post>(p => captured = p), ...). After the act phase, assert on captured. This is more readable than Moq’s Callback because the capture logic is inline with the argument specification.
Warning: NSubstitute’s received call verification must come after the act phase — you cannot set up “expect this call” before the test runs (unlike Moq where you set up the behaviour first and verify after). NSubstitute is checking what actually happened: Received() is a post-hoc assertion. This is generally cleaner (Arrange-Act-Assert is more explicit) but means forgetting to call Received in the Assert phase silently skips the verification, unlike Moq’s VerifyAll which catches unverified setups.

Common Mistakes

Mistake 1 — Forgetting Received() assertion (verification silently skipped)

❌ Wrong — test passes without verifying a critical call was made; the interaction check was intended but forgotten.

✅ Correct — explicitly call await substitute.Received(1).MethodName(...) in the Assert phase for every interaction you care about.

Mistake 2 — Mixing NSubstitute and Moq in the same test project (inconsistent patterns)

❌ Wrong — some tests use Moq, others use NSubstitute; team members must know both APIs; inconsistent style.

✅ Correct — choose one library for the project and use it consistently; document the choice in the project README.

🧠 Test Yourself

NSubstitute: repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(post). The service calls repo.GetByIdAsync(2, ct). What does NSubstitute return?