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 matchStrict 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
.Objectwrapper 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.