Mocking in .NET: Moq vs NSubstitute for Unit Testing

Mocking in .NET: Moq vs NSubstitute for Unit Testing

Unit tests that hit real databases, external APIs, or file systems are slow, brittle, and environment-dependent. Mocking frameworks let you replace real dependencies with controlled fakes. In .NET, two frameworks dominate: Moq and NSubstitute.

Both work. The differences are real but not dramatic. This guide covers practical usage of both so you can make an informed choice and use either effectively.

The Core Problem They Solve

Consider an order service that sends emails:

public class OrderService
{
    private readonly IEmailService _emailService;
    private readonly IOrderRepository _repository;
    
    public OrderService(IEmailService emailService, IOrderRepository repository)
    {
        _emailService = emailService;
        _repository = repository;
    }
    
    public async Task<Order> CreateAsync(CreateOrderRequest request)
    {
        var order = new Order(request);
        await _repository.SaveAsync(order);
        await _emailService.SendConfirmationAsync(order);
        return order;
    }
}

Testing CreateAsync with a real IEmailService sends actual emails. With a real IOrderRepository, you need a database. Mocking replaces both with controlled fakes:

// With Moq
var emailMock = new Mock<IEmailService>();
var repoMock = new Mock<IOrderRepository>();

var service = new OrderService(emailMock.Object, repoMock.Object);
// With NSubstitute
var emailSub = Substitute.For<IEmailService>();
var repoSub = Substitute.For<IOrderRepository>();

var service = new OrderService(emailSub, repoSub);

The key difference is visible immediately: Moq wraps the mock in a Mock<T> wrapper and you use .Object to get the actual instance. NSubstitute returns the mock directly — no wrapper.

Setting Up Return Values

Moq

var mock = new Mock<IUserRepository>();

// Simple return
mock.Setup(x => x.GetByIdAsync(42))
    .ReturnsAsync(new User { Id = 42, Name = "Alice" });

// Return based on input
mock.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
    .ReturnsAsync((int id) => new User { Id = id, Name = $"User {id}" });

// Return different values on successive calls
mock.SetupSequence(x => x.GetNextIdAsync())
    .ReturnsAsync(1)
    .ReturnsAsync(2)
    .ReturnsAsync(3);

// Throw an exception
mock.Setup(x => x.GetByIdAsync(-1))
    .ThrowsAsync(new NotFoundException("User not found"));

// Properties
mock.Setup(x => x.ConnectionString).Returns("Server=localhost");
mock.SetupProperty(x => x.IsConnected, true);

NSubstitute

var sub = Substitute.For<IUserRepository>();

// Simple return
sub.GetByIdAsync(42).Returns(new User { Id = 42, Name = "Alice" });

// Return based on input
sub.GetByIdAsync(Arg.Any<int>()).Returns(callInfo => 
    new User { Id = callInfo.Arg<int>(), Name = $"User {callInfo.Arg<int>()}" });

// Return different values on successive calls
sub.GetNextIdAsync().Returns(Task.FromResult(1), Task.FromResult(2), Task.FromResult(3));

// Throw an exception
sub.GetByIdAsync(-1).Returns<User>(_ => throw new NotFoundException("User not found"));

// Properties
sub.ConnectionString.Returns("Server=localhost");

NSubstitute's syntax is more natural — it looks like calling the actual method. Moq's lambda-based Setup(x => x.Method(...)) is more explicit but more verbose.

Argument Matching

Both frameworks provide argument matchers for flexible setup.

Moq Argument Matching

var mock = new Mock<IOrderService>();

// Any value
mock.Setup(x => x.Process(It.IsAny<Order>())).Returns(true);

// Predicate match
mock.Setup(x => x.Process(It.Is<Order>(o => o.Total > 100)))
    .Returns(true);

// Type match
mock.Setup(x => x.Process(It.IsAny<PriorityOrder>())).Returns(true);

// Range
mock.Setup(x => x.Discount(It.IsInRange(1, 100, Range.Inclusive)))
    .Returns(0.1m);

// Regex for strings
mock.Setup(x => x.FindUser(It.IsRegex("^admin")))
    .Returns(adminUser);

NSubstitute Argument Matching

var sub = Substitute.For<IOrderService>();

// Any value
sub.Process(Arg.Any<Order>()).Returns(true);

// Predicate match
sub.Process(Arg.Is<Order>(o => o.Total > 100)).Returns(true);

// Type match
sub.Process(Arg.Any<PriorityOrder>()).Returns(true);

// Do action when called (useful for capturing arguments)
sub.When(x => x.Process(Arg.Any<Order>()))
   .Do(callInfo => capturedOrder = callInfo.Arg<Order>());

The matchers are functionally equivalent. NSubstitute's Arg.Is<T>() and Moq's It.Is<T>() do the same thing.

Verification

After the test action, verify that dependencies were called as expected.

Moq Verification

var emailMock = new Mock<IEmailService>();

// Verify called once with any argument
emailMock.Verify(x => x.SendAsync(It.IsAny<string>()), Times.Once());

// Verify called with specific argument
emailMock.Verify(x => x.SendAsync("alice@example.com"), Times.Once());

// Verify called N times
emailMock.Verify(x => x.SendAsync(It.IsAny<string>()), Times.Exactly(3));

// Verify never called
emailMock.Verify(x => x.SendAsync(It.IsAny<string>()), Times.Never());

// Verify at least N times
emailMock.Verify(x => x.SendAsync(It.IsAny<string>()), Times.AtLeast(1));

// Verify all setups were called (strict mode)
emailMock.VerifyAll();

// Verify no unexpected calls
emailMock.VerifyNoOtherCalls();

NSubstitute Verification

var emailSub = Substitute.For<IEmailService>();

// Verify called once with any argument
emailSub.Received(1).SendAsync(Arg.Any<string>());

// Verify called with specific argument
emailSub.Received(1).SendAsync("alice@example.com");

// Verify never called
emailSub.DidNotReceive().SendAsync(Arg.Any<string>());

// Verify at least once
emailSub.Received().SendAsync(Arg.Any<string>());

// Capture and assert the argument
var calls = emailSub.ReceivedCalls();
var sendCall = calls.First(c => c.GetMethodInfo().Name == "SendAsync");
Assert.Equal("alice@example.com", sendCall.GetArguments()[0]);

NSubstitute's verification syntax is more natural — Received() reads like English. Moq's Verify() with Times is more explicit about count.

Async Mocking

Both frameworks handle async well, but the syntax differs slightly.

Moq Async

var mock = new Mock<IUserRepository>();

// Returns a completed task
mock.Setup(x => x.GetByIdAsync(42))
    .ReturnsAsync(new User { Id = 42 });

// Returns a delayed task (simulating latency)
mock.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
    .Returns(async () => {
        await Task.Delay(10);
        return new User();
    });

// Verify async calls
mock.Verify(x => x.GetByIdAsync(42), Times.Once());

NSubstitute Async

var sub = Substitute.For<IUserRepository>();

// Returns a completed task
sub.GetByIdAsync(42).Returns(new User { Id = 42 });

// Returns explicit Task
sub.GetByIdAsync(42).Returns(Task.FromResult(new User { Id = 42 }));

// Simulate delay
sub.GetByIdAsync(Arg.Any<int>()).Returns(async (callInfo) => {
    await Task.Delay(10);
    return new User();
});

// Verify async calls
await sub.Received(1).GetByIdAsync(42);

NSubstitute requires await on async verifications to capture the correct call. Missing await can cause false passes.

Callback and Capture Patterns

Sometimes you need to capture what was passed to a mock, not just verify it was called.

Moq Callback

Order capturedOrder = null;

var repoMock = new Mock<IOrderRepository>();
repoMock.Setup(x => x.SaveAsync(It.IsAny<Order>()))
        .Callback<Order>(order => capturedOrder = order)
        .ReturnsAsync(true);

// Execute code under test
await _service.CreateOrderAsync(request);

// Assert the captured value
Assert.Equal(request.CustomerId, capturedOrder.CustomerId);
Assert.Equal(OrderStatus.Pending, capturedOrder.Status);

NSubstitute Callback

Order capturedOrder = null;

var repoSub = Substitute.For<IOrderRepository>();
repoSub.When(x => x.SaveAsync(Arg.Any<Order>()))
       .Do(callInfo => capturedOrder = callInfo.Arg<Order>());
repoSub.SaveAsync(Arg.Any<Order>()).Returns(Task.FromResult(true));

// Execute code under test
await _service.CreateOrderAsync(request);

// Assert the captured value
Assert.Equal(request.CustomerId, capturedOrder.CustomerId);

Strict vs Loose Mocks

By default, both frameworks use loose mocks — unmocked methods return default values (null, 0, false).

Moq Strict Mode

// Strict: any call not explicitly set up throws MockException
var strictMock = new Mock<IEmailService>(MockBehavior.Strict);

// Must set up every call that will be made
strictMock.Setup(x => x.SendAsync(It.IsAny<string>())).ReturnsAsync(true);

// This will throw if SendAsync is called with an argument that doesn't match

Strict mode catches unexpected calls early. Use it when you want to explicitly control every interaction with a dependency.

NSubstitute Strict (via NSubstitute.Analyzers or conventions)

NSubstitute doesn't have a built-in strict mode, but NSubstitute.Analyzers provides compile-time warnings for unexpected substitute usage.

Common Patterns

Repository Pattern with Both Frameworks

// Moq - Repository pattern
public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repoMock;
    private readonly UserService _sut;

    public UserServiceTests()
    {
        _repoMock = new Mock<IUserRepository>();
        _sut = new UserService(_repoMock.Object);
    }

    [Fact]
    public async Task GetUser_ExistingId_ReturnsUser()
    {
        var expected = new User { Id = 1, Name = "Alice" };
        _repoMock.Setup(x => x.GetByIdAsync(1)).ReturnsAsync(expected);

        var result = await _sut.GetUserAsync(1);

        Assert.Equal(expected.Name, result.Name);
        _repoMock.Verify(x => x.GetByIdAsync(1), Times.Once);
    }
}

// NSubstitute - same test
public class UserServiceTests
{
    private readonly IUserRepository _repo;
    private readonly UserService _sut;

    public UserServiceTests()
    {
        _repo = Substitute.For<IUserRepository>();
        _sut = new UserService(_repo);
    }

    [Fact]
    public async Task GetUser_ExistingId_ReturnsUser()
    {
        var expected = new User { Id = 1, Name = "Alice" };
        _repo.GetByIdAsync(1).Returns(expected);

        var result = await _sut.GetUserAsync(1);

        Assert.Equal(expected.Name, result.Name);
        await _repo.Received(1).GetByIdAsync(1);
    }
}

When to Use Each

Choose Moq if:

  • Your team is already using Moq (switching costs are real)
  • You want explicit setup syntax that's clearly separate from production code
  • You need strict mode for catching unexpected calls
  • The existing community examples and documentation are in Moq

Choose NSubstitute if:

  • You prefer more natural, readable test syntax
  • You're setting up a new project without existing investment
  • You find Moq's .Object wrapper and lambda syntax verbose
  • Your team includes developers new to mocking who will find NSubstitute's syntax more intuitive

In practice: The choice matters less than using your chosen framework consistently and well. Teams that switch between them mid-project suffer more than teams that stick with either.

What Mocks Cannot Replace

Over-mocking is a real problem. When you mock everything, tests verify that your code calls its dependencies in the expected order — not that the system actually works.

Tests that should not use mocks:

  • Integration tests: hit the real database, real cache, real queue
  • Contract tests: verify the real API returns the expected format
  • End-to-end tests: run the whole system

Mocks belong in unit tests that isolate a single class from its dependencies. Combine unit tests (mocks) with integration tests (real dependencies) for complete coverage.

For always-on production validation — confirming the live system behaves as your mocked unit tests expect — HelpMeTest monitors API and UI behavior continuously, catching the drift that mocks can't detect.

Summary

Moq and NSubstitute both solve the same problem effectively. Moq has more explicit syntax and strict mode. NSubstitute has more natural syntax and reads like real code.

The patterns — setup, verify, argument matching, async support — are present in both. Learn one well rather than jumping between them. The testing discipline matters more than the framework: isolate one dependency at a time, verify behavior not implementation, and complement mocked unit tests with integration tests against real systems.

Start now free