Mocking Frameworks: Simulating Dependencies in Tests
A practical guide to mocking frameworks in .NET — Moq and NSubstitute — covering what a test double actually is, the different kinds (dummy, stub, spy, mock, fake), core usage of both libraries side by side, argument matching, verifying interactions, and the honest signals that tell you when mocking is helping versus when it's masking a design problem.
Table of Contents
- Introduction
- Test Doubles: Dummy, Stub, Spy, Mock, Fake
- Moq: Core Usage
- NSubstitute: Core Usage
- Moq vs. NSubstitute, Side by Side
- Argument Matching
- Verifying Interactions
- Mocking Return Sequences and Callbacks
- What Can (and Can't) Be Mocked
- Auto-Mocking Containers
- When Heavy Mocking Signals a Design Problem
- Mocks vs. Fakes vs. Testcontainers
- Common Pitfalls
- Quick Reference Table
- Conclusion
Introduction
A mocking framework lets a unit test replace a class's real dependencies with configurable, observable substitutes — so a test can verify the class's own logic in isolation, without needing a real database, a real HTTP call, or any other genuine side effect. This series' xUnit guide introduced Moq briefly as part of testing a handler in isolation; this guide gives the topic its full treatment — the vocabulary for different kinds of test doubles, Moq and NSubstitute covered side by side (the two most widely used .NET mocking libraries, with genuinely different design philosophies), and — consistent with this series' recurring theme of matching a tool to a genuine need — an honest treatment of when heavy mocking is a sign of good test isolation versus a sign of a design that needs rethinking.
// Moq
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync(new Order());
// NSubstitute — the same idea, a different syntax philosophy
var repo = Substitute.For<IOrderRepository>();
repo.GetByIdAsync(Arg.Any<int>()).Returns(new Order());
Both achieve the same result — a fake IOrderRepository that returns a configured Order when asked — via meaningfully different syntax, covered throughout this guide.
1. Test Doubles: Dummy, Stub, Spy, Mock, Fake
"Mock" is often used loosely to mean any test double — worth being precise
public interface IEmailService { Task SendAsync(string to, string subject, string body); }
The general term for any object that stands in for a real dependency in a test is a test double (a term borrowed from stunt doubles in film) — "mock" is commonly used loosely to refer to all of them, but the more precise vocabulary, worth knowing because it clarifies what a given test is actually verifying, distinguishes five kinds.
Dummy: passed in but never actually used
var dummyLogger = new Mock<ILogger<OrderService>>().Object; // required by the constructor, but this test never checks it
var service = new OrderService(dummyLogger, realRepository);
A dummy exists purely to satisfy a constructor or method signature's parameter list — the test doesn't care what it does or configure any behavior on it at all, since the code path under test never actually exercises it meaningfully.
Stub: returns configured, canned answers
var stubRepository = new Mock<IOrderRepository>();
stubRepository.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(new Order { Id = 1, Total = 149.97m });
A stub provides pre-configured, canned responses to specific calls — the test uses it purely to control what the class under test receives back, without caring whether or how many times the stub's methods were actually called.
Spy: records how it was used, for later inspection
var spyEmailService = new FakeEmailService(); // a hand-written test double that records calls
await handler.Handle(command);
Assert.Single(spyEmailService.SentEmails); // inspects what actually happened, after the fact
A spy records information about how it was called (arguments, call count) so the test can inspect that record afterward — the emphasis is on later inspection of recorded facts, rather than the test explicitly asserting an expectation was met as part of the mock's own API.
Mock (in the strict sense): verifies an expected interaction actually occurred
var mockEmailService = new Mock<IEmailService>();
await handler.Handle(command);
mockEmailService.Verify(e => e.SendAsync("ada@example.com", It.IsAny<string>(), It.IsAny<string>()), Times.Once);
A mock, in the strict, original sense, is a test double that the test explicitly asks to verify a specific interaction happened — Verify(...) is asserting a behavioral expectation ("this specific call should have happened exactly once"), which is a genuinely different kind of assertion than checking a stub's return value or inspecting a spy's recorded history after the fact.
Fake: a real, working (but simplified) implementation
public class InMemoryOrderRepository : IOrderRepository
{
private readonly Dictionary<int, Order> _orders = new();
public Task<Order?> GetByIdAsync(int id) => Task.FromResult(_orders.GetValueOrDefault(id));
public Task AddAsync(Order order) { _orders[order.Id] = order; return Task.CompletedTask; }
}
A fake is a genuine, working implementation — just a simplified one, unsuitable for production (an in-memory dictionary instead of a real database) but behaviorally real within the test's scope, rather than a framework-generated stand-in with explicitly configured canned responses. Section 11 covers when reaching for a fake is a better fit than a mocking-framework-generated double.
Why this vocabulary is worth knowing, beyond pedantry
Being precise about which kind of double a test actually needs clarifies what the test is genuinely verifying — a test asserting on a stub's return value is checking the class under test's own logic given known input; a test using Verify on a mock is checking that the class under test correctly calls its dependencies, a meaningfully different (and, per Section 10, sometimes overused) kind of assertion.
2. Moq: Core Usage
Creating a mock and configuring behavior
var mockRepository = new Mock<IOrderRepository>();
mockRepository
.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
.ReturnsAsync(new Order { Id = 1, Total = 149.97m });
var repository = mockRepository.Object; // the actual IOrderRepository instance to inject
Moq's central type is Mock<T>, wrapping the interface being mocked — .Setup(...) configures behavior for a specific method call pattern, .ReturnsAsync(...) (or .Returns(...) for synchronous methods) specifies what that call should return, and .Object exposes the actual mocked instance to pass into the class under test's constructor.
Configuring a method that throws
mockRepository
.Setup(r => r.GetByIdAsync(999))
.ThrowsAsync(new InvalidOperationException("Simulated database failure"));
As covered in this series' xUnit guide, this is one of mocking's most valuable capabilities — deterministically simulating a failure that would be awkward or unreliable to reproduce against a real dependency, letting a test thoroughly exercise error-handling logic.
Verifying a method was called
mockRepository.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
mockRepository.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Never); // asserting it was NOT called
Verify is Moq's mechanism for the strict "mock" assertion covered in Section 1 — confirming a specific interaction genuinely occurred (or explicitly didn't), with Times.Once, Times.Never, Times.Exactly(n), Times.AtLeast(n), and several other cardinality options.
Mocking properties
var mockConfig = new Mock<IAppConfiguration>();
mockConfig.SetupGet(c => c.MaxRetryAttempts).Returns(3);
mockConfig.SetupProperty(c => c.CurrentEnvironment, "Test"); // a settable property, tracked with normal get/set semantics
Moq distinguishes read-only property mocking (SetupGet) from a genuinely stateful, settable property (SetupProperty, which lets the mock behave like a real backing field, remembering whatever value is set to it) — worth knowing since the two produce meaningfully different behavior if a test both reads and writes the same mocked property.
Mocking class members (not just interfaces)
var mockService = new Mock<OrderService>(); // requires the class's members to be `virtual`
mockService.Setup(s => s.CalculateDiscount(It.IsAny<Order>())).Returns(0.1m);
Moq can mock a concrete class, but only members explicitly marked virtual (or interface members) can actually be overridden — this is a genuine design constraint worth knowing, and it's a large part of why the interface-based dependency style covered throughout this series' DDD and Vertical Slices guides pairs so naturally with mocking frameworks generally: interfaces are mockable by construction, with no special modifiers required.
3. NSubstitute: Core Usage
The same capability, a deliberately different syntax philosophy
var repository = Substitute.For<IOrderRepository>();
repository.GetByIdAsync(Arg.Any<int>()).Returns(new Order { Id = 1, Total = 149.97m });
NSubstitute's central design goal is reading like plain, natural C# rather than a fluent configuration API — Substitute.For<T>() creates the substitute directly (no separate Mock<T> wrapper object with a .Object property to unwrap), and configuring a return value looks almost exactly like calling the real method and describing what it should return, rather than Moq's .Setup(...).Returns(...) two-step expression.
Configuring a method that throws
repository.GetByIdAsync(999).Returns<Order>(x => throw new InvalidOperationException("Simulated database failure"));
// or, more idiomatically for NSubstitute:
repository.When(r => r.GetByIdAsync(999)).Do(x => throw new InvalidOperationException("Simulated database failure"));
Verifying (called "received") in NSubstitute's vocabulary
await repository.Received(1).AddAsync(Arg.Any<Order>());
await repository.DidNotReceive().DeleteAsync(Arg.Any<int>());
NSubstitute calls verification "received" rather than Moq's "verify" — .Received(1) (or without an argument, defaulting to "at least once") reads, deliberately, almost like an English sentence ("the repository received a call to AddAsync"), which is precisely NSubstitute's core design philosophy applied consistently across every part of its API.
Configuring and verifying properties
var config = Substitute.For<IAppConfiguration>();
config.MaxRetryAttempts.Returns(3); // reads exactly like accessing a real property
config.CurrentEnvironment = "Test"; // NSubstitute's substitutes support real property get/set semantics natively
Assert.Equal("Test", config.CurrentEnvironment);
NSubstitute's substitutes support genuine, automatic property getter/setter behavior without Moq's SetupGet/SetupProperty distinction — a property on a substitute just behaves like a real, stateful property by default, which is part of NSubstitute's broader design bet that mocking syntax should require as little dedicated, framework-specific vocabulary as possible.
Partial substitutes for concrete classes
var service = Substitute.ForPartsOf<OrderService>();
service.CalculateDiscount(Arg.Any<Order>()).Returns(0.1m); // still requires virtual members, same constraint as Moq
NSubstitute's equivalent capability for mocking concrete classes carries the identical virtual-member constraint Moq has — this isn't a difference between the two libraries; it's a fundamental .NET runtime constraint (dynamic proxy generation can only override virtual/interface members) that both libraries are equally subject to.
4. Moq vs. NSubstitute, Side by Side
The same test, written in both
// Moq
[Fact]
public async Task CancelOrder_ReturnsFailure_WhenOrderNotFound()
{
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync((Order?)null);
var handler = new CancelOrderHandler(mockRepo.Object);
var result = await handler.Handle(new CancelOrderCommand(999), CancellationToken.None);
Assert.False(result.IsSuccess);
mockRepo.Verify(r => r.GetByIdAsync(999), Times.Once);
}
// NSubstitute
[Fact]
public async Task CancelOrder_ReturnsFailure_WhenOrderNotFound()
{
var repo = Substitute.For<IOrderRepository>();
repo.GetByIdAsync(Arg.Any<int>()).Returns((Order?)null);
var handler = new CancelOrderHandler(repo);
var result = await handler.Handle(new CancelOrderCommand(999), CancellationToken.None);
Assert.False(result.IsSuccess);
await repo.Received(1).GetByIdAsync(999);
}
The genuine differences, stated plainly
| Moq | NSubstitute | |
|---|---|---|
| Creation |
new Mock<T>(), then .Object to get the instance |
Substitute.For<T>() returns the instance directly |
| Configuring returns | .Setup(x => x.Method()).Returns(value) |
substitute.Method().Returns(value) — reads like a real call |
| Verifying calls | .Verify(x => x.Method(), Times.Once) |
substitute.Received(1).Method() |
| Property mocking | Distinguishes SetupGet/SetupProperty
|
Properties behave like real, stateful properties automatically |
| Design philosophy | An explicit, fluent configuration API, separate from the mocked instance itself | Reads as close to plain, natural C# as the language allows |
Neither is objectively superior — this is a genuine style preference
Both libraries are mature, widely adopted, well-maintained, and functionally comparable for the overwhelming majority of testing needs — the choice between them is largely a team's syntax preference (Moq's explicit, separate configuration API vs. NSubstitute's closer-to-natural-C# style), not a meaningful capability gap in either direction. Teams should pick one and use it consistently across a codebase rather than mixing both, purely for consistency's sake, not because one is objectively better suited to any specific scenario the other genuinely can't handle.
5. Argument Matching
Matching any argument of a given type
// Moq
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync(order);
// NSubstitute
repo.GetByIdAsync(Arg.Any<int>()).Returns(order);
The most common matcher — configuring behavior regardless of the specific argument value passed, useful when a test doesn't care about the exact input, only that some call with an argument of that type occurs.
Matching a specific value
// Moq
mockRepo.Setup(r => r.GetByIdAsync(42)).ReturnsAsync(specificOrder);
// NSubstitute
repo.GetByIdAsync(42).Returns(specificOrder);
Both libraries let you configure genuinely different behavior for different specific argument values on the same mocked method — calling GetByIdAsync(42) returns one configured order, while GetByIdAsync(999) (per Section 2's example) returns null or throws, letting one test double express multiple, distinct scenarios simultaneously.
Matching with a predicate
// Moq
mockRepo.Setup(r => r.GetByIdAsync(It.Is<int>(id => id > 0))).ReturnsAsync(order);
// NSubstitute
repo.GetByIdAsync(Arg.Is<int>(id => id > 0)).Returns(order);
For genuinely conditional matching beyond an exact value or "any," both libraries support predicate-based matchers — useful for asserting a more nuanced expectation (any positive ID, any string starting with a specific prefix) without needing to enumerate every possible matching value explicitly.
Capturing the actual argument for further inspection
// Moq
Order? capturedOrder = null;
mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
.Callback<Order>(order => capturedOrder = order)
.Returns(Task.CompletedTask);
await handler.Handle(command, CancellationToken.None);
Assert.Equal(42, capturedOrder?.CustomerId);
// NSubstitute
await handler.Handle(command, CancellationToken.None);
var capturedOrder = repository.ReceivedCalls()
.First(c => c.GetMethodInfo().Name == nameof(IOrderRepository.AddAsync))
.GetArguments()[0] as Order;
Assert.Equal(42, capturedOrder?.CustomerId);
Sometimes a test needs to inspect the actual argument a dependency was called with, beyond simply matching it — Moq's Callback<T> is the more commonly reached-for mechanism for this; NSubstitute's ReceivedCalls() provides equivalent access, though (as this example shows) somewhat less directly for this specific pattern, which is one of the few areas where the two libraries' ergonomics genuinely diverge rather than being purely a syntax-style difference.
6. Verifying Interactions
Verifying call count precisely
// Moq
mockEmailService.Verify(e => e.SendAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(2));
// NSubstitute
await emailService.Received(2).SendAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
Verifying no unexpected calls occurred at all
// Moq
mockEmailService.VerifyNoOtherCalls(); // fails if ANY call happened beyond what was explicitly verified above
// NSubstitute
emailService.ReceivedCalls().Should().HaveCount(1); // via FluentAssertions, or manual enumeration
VerifyNoOtherCalls() (Moq) is a genuinely strict assertion — useful specifically when a test needs to confirm the class under test interacted with a dependency in exactly the expected way, and nothing more; NSubstitute doesn't have a precisely equivalent single-method call, though the same intent is achievable by inspecting ReceivedCalls() directly.
Verifying call order
// Moq, via a MockSequence
var sequence = new MockSequence();
mockRepo.InSequence(sequence).Setup(r => r.GetByIdAsync(1)).ReturnsAsync(order);
mockRepo.InSequence(sequence).Setup(r => r.DeleteAsync(1)).Returns(Task.CompletedTask);
Both libraries support asserting that calls happened in a specific relative order (rarely needed, but occasionally genuinely important — confirming a resource was fetched before it was deleted, for instance) — this is a more advanced, less commonly reached-for capability worth knowing exists rather than a routine part of everyday test-writing.
The judgment call: how strict should verification be?
Over-specifying exactly which calls happen, in what order, with what exact arguments, risks producing a test so tightly coupled to the implementation of the class under test that any reasonable refactor (even one that preserves correct behavior) breaks the test — the general, widely-shared guidance is to verify only the interactions that are genuinely part of the behavioral contract worth protecting (an email was sent, an order was persisted), not every incidental detail of exactly how the class under test happens to currently be implemented.
7. Mocking Return Sequences and Callbacks
Returning different values on successive calls
// Moq
mockRepo.SetupSequence(r => r.GetNextIdAsync())
.ReturnsAsync(1)
.ReturnsAsync(2)
.ReturnsAsync(3);
// NSubstitute
repo.GetNextIdAsync().Returns(1, 2, 3); // successive calls return each value in order
Useful for testing code that calls the same dependency method repeatedly and expects a genuinely changing sequence of results — a retry loop, a paginated fetch, an incrementing ID generator.
Executing custom logic via a callback
// Moq
mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
.Callback<Order>(order => order.Id = 42) // simulates the database assigning an ID on insert
.Returns(Task.CompletedTask);
// NSubstitute
repository.AddAsync(Arg.Do<Order>(order => order.Id = 42));
A callback lets a test double do something beyond simply returning a value — here, simulating a database's real behavior of assigning a generated ID to an entity upon insertion, letting the rest of the test proceed as though that had genuinely happened.
8. What Can (and Can't) Be Mocked
Interfaces: always mockable
public interface IOrderRepository { /* ... */ } // trivially mockable by either library
Interfaces are the ideal, friction-free case for both libraries — no special modifiers needed, and this is precisely why the interface-based abstraction style covered throughout this series' DDD, Repository (Design Patterns guide), and Vertical Slices guides pairs so naturally with mocking: every dependency expressed as an interface is automatically, fully mockable.
Virtual class members: mockable, with the constraint stated plainly
public class OrderService
{
public virtual decimal CalculateTotal(Order order) => order.LineItems.Sum(li => li.Subtotal); // mockable
public decimal CalculateTax(Order order) => CalculateTotal(order) * 0.08m; // NOT mockable — not virtual
}
As covered in Sections 2 and 3, both libraries can only override virtual (or abstract) members on a concrete class — a non-virtual method simply cannot be intercepted by either library's proxy-generation mechanism, a fundamental .NET constraint, not a specific library limitation.
Sealed classes and static methods: not mockable by either library at all
public sealed class OrderCalculator { public decimal Calculate(Order order) => /* ... */; } // cannot be mocked
public static class DateTimeProvider { public static DateTime UtcNow => DateTime.UtcNow; } // cannot be mocked directly
Neither Moq nor NSubstitute can mock a sealed class at all, or a static method directly — this is a genuine, structural limitation both libraries share, and it's precisely why code depending on DateTime.UtcNow directly, or any other static, non-overridable dependency, is hard to unit test in isolation; the standard mitigation is wrapping the static dependency behind your own injectable interface (ITimeProvider or, in modern .NET, the built-in TimeProvider abstraction) specifically so it becomes mockable.
The practical implication for how you design dependencies
This is a direct, practical argument for the interface-heavy design style this series has covered throughout its DDD and Vertical Slices guides — not purely for architectural elegance, but because designing dependencies as interfaces from the start is what keeps a class genuinely, easily unit-testable in isolation later, without needing an awkward retrofit once a test reveals a static or sealed dependency can't be substituted.
9. Auto-Mocking Containers
The problem: constructing a class with many dependencies means mocking every single one manually
// A handler with several dependencies means several separate Mock<T>/Substitute.For<T> calls,
// even for tests that only care about ONE of them
var mockRepo = new Mock<IOrderRepository>();
var mockEmail = new Mock<IEmailService>();
var mockPayment = new Mock<IPaymentService>();
var mockLogger = new Mock<ILogger<PlaceOrderHandler>>();
var handler = new PlaceOrderHandler(mockRepo.Object, mockEmail.Object, mockPayment.Object, mockLogger.Object);
As covered in this series' Vertical Slices guide, a class with many dependencies produces correspondingly verbose test setup, even when a specific test only genuinely cares about one or two of them — every additional constructor parameter means one more mock to construct and thread through, in every single test for that class.
Auto-mocking containers as a targeted convenience
// Using AutoFixture with AutoMoq, as one example of this category of tool
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var handler = fixture.Create<PlaceOrderHandler>(); // automatically constructs and injects mocks for every dependency
var mockRepo = fixture.Freeze<Mock<IOrderRepository>>(); // grab a reference to configure/verify a SPECIFIC one
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync(testOrder);
Libraries like AutoFixture (paired with AutoMoq or an NSubstitute equivalent) automatically construct a class under test with auto-generated mocks for every constructor dependency, letting a test "freeze" and configure only the specific one or two dependencies it actually cares about, while the rest are automatically supplied as harmless, unconfigured dummies.
The honest trade-off this convenience introduces
This genuinely reduces boilerplate for classes with many dependencies — but it's worth being aware it can also hide the signal Section 10 covers next: a class needing an auto-mocking container just to keep its tests readable may be a class that's accumulated more dependencies than it should have in the first place, and the convenience tool can mask that signal rather than prompting a reconsideration of the design.
10. When Heavy Mocking Signals a Design Problem
The genuine, recurring signal worth taking seriously
// A test needing THIS much mock setup just to exercise one specific code path
var mockRepo = new Mock<IOrderRepository>();
var mockInventory = new Mock<IInventoryService>();
var mockPayment = new Mock<IPaymentService>();
var mockEmail = new Mock<IEmailService>();
var mockAnalytics = new Mock<IAnalyticsService>();
var mockAudit = new Mock<IAuditLogger>();
// ... six mocks, just to test "does placing an order with an invalid discount code fail correctly"
As covered in this series' xUnit guide and echoed throughout the Vertical Slices and Design Patterns guides, when a test's mock setup is longer and more complex than the actual logic being verified, that's a genuine design smell — not a mocking-framework limitation to work around with a bigger convenience tool (Section 9), but a signal that the class under test may have accumulated more responsibilities and dependencies than a single, focused unit genuinely needs.
What this often actually indicates
- The class has too many responsibilities — echoing this series' Vertical Slices guide's observation that a shared service class accumulating the union of every method's dependencies is a common, specific cause of exactly this pattern.
-
A missing abstraction — several related dependencies (inventory, payment, shipping) might genuinely belong behind one cohesive domain concept (an
OrderFulfillmentService, or better, logic properly encapsulated in a DDD aggregate per this series' DDD guide) rather than being individually injected and separately mocked. - Testing at the wrong level — a scenario this complex might be more honestly and more valuably verified as an integration test (per this series' Integration Tests guide) against real components, rather than forced into a unit test straining under an ever-growing pile of mocks trying to simulate all of them.
The constructive response: listen to the signal, don't just add more mocking tooling
The recurring, consistent guidance across this series' testing-adjacent content: when a test's mocking burden feels disproportionate to the logic under test, the corrective action worth trying first is reconsidering the class's own design — extracting a smaller aggregate (per the DDD guide), splitting a bloated handler (per the Vertical Slices guide), or reconsidering whether this scenario is genuinely a unit-test concern at all — rather than reaching for an auto-mocking container purely to make the existing, overly-broad design's tests more bearable to write.
11. Mocks vs. Fakes vs. Testcontainers
Three genuinely different tools for isolating a dependency, at different fidelity/speed points
Mock (Moq/NSubstitute): fastest, zero real behavior, purely configured responses — for pure unit tests
Fake (hand-written, in-memory): fast, genuinely working simplified logic — a middle ground
Testcontainers (per this series' companion guide): slowest per-suite-startup, but the REAL dependency — for integration tests
This is a direct, practical synthesis connecting this guide to this series' Testcontainers and Integration Tests guides — mocks are the right tool when a test genuinely only needs to verify the class under test's own logic, given known, controlled responses from its dependencies; a hand-written fake (per Section 1) is worth reaching for when a dependency's actual, simplified-but-real behavior matters more than just a canned answer (an in-memory repository that genuinely stores and retrieves objects, rather than a mock that only returns whatever was explicitly configured); and Testcontainers is the right tool the moment a test's actual purpose is verifying real integration with a real, genuine dependency, per this series' Integration Tests guide's core argument.
Choosing among them per test, not per project
A single, well-structured test suite typically uses all three, each in the layer of the testing pyramid (per this series' CI/CD Pipelines and xUnit guides) where it fits: fast, numerous unit tests using mocks at the base; a smaller layer of integration tests using Testcontainers verifying real component interaction; and, occasionally, hand-written fakes as a lightweight middle ground for dependencies whose simplified-but-genuine behavior is easier to reason about than an elaborately configured mock would be.
12. Common Pitfalls
| Pitfall | Why it hurts | Better approach |
|---|---|---|
| Over-specifying exact call order/arguments for interactions that aren't genuinely part of the contract | Tests break on any reasonable refactor, even ones preserving correct behavior | Verify only the interactions that are genuinely behaviorally significant |
| Reaching for an auto-mocking container to paper over a class with too many dependencies | Masks the actual design signal rather than addressing it | Treat excessive mock setup as a prompt to reconsider the class's own design first |
| Mocking a dependency that should instead be a hand-written fake with real, simplified behavior | Produces a test that verifies "the mock does what I configured," not genuine logic | Use a fake when the dependency's actual behavior (not just a canned response) matters to the test |
| Assuming Moq and NSubstitute differ in capability, not just syntax | Leads to choosing one over the other for the wrong reasons | Recognize the choice as largely a team style preference; pick one and use it consistently |
| Trying to mock a sealed class or a static method directly | Neither library can do this; wasted effort | Wrap the static/sealed dependency behind your own injectable interface first |
| Confusing a stub (canned response) with a mock (verified interaction) conceptually | Leads to unclear tests that don't communicate what's actually being verified | Be deliberate about whether a given test is checking a return value or a behavioral expectation |
| Using mocks for scenarios that are genuinely integration concerns | The test provides false confidence that real components actually work together | Use Testcontainers-based integration tests, per this series' companion guide, for genuine integration verification |
Quick Reference Table
| Concept | Moq | NSubstitute |
|---|---|---|
| Create a test double |
new Mock<T>(), use .Object
|
Substitute.For<T>() |
| Configure a return value | .Setup(x => x.M()).Returns(v) |
sub.M().Returns(v) |
| Configure a throw | .Setup(...).Throws(ex) |
sub.When(x => x.M()).Do(x => throw ex) |
| Verify a call happened | .Verify(x => x.M(), Times.Once) |
sub.Received(1).M() |
| Argument matcher (any) | It.IsAny<T>() |
Arg.Any<T>() |
| Argument matcher (predicate) | It.Is<T>(predicate) |
Arg.Is<T>(predicate) |
| Return sequence | .SetupSequence(...) |
sub.M().Returns(v1, v2, v3) |
| Design philosophy | Explicit, fluent configuration API | Reads as close to plain C# as possible |
Conclusion
Moq and NSubstitute solve the identical problem — isolating a class under test from its real dependencies — through genuinely different syntax philosophies, and the choice between them is a team preference, not a capability trade-off; both handle the core needs covered throughout this guide (configuring returns, simulating failures, verifying interactions, matching arguments) equally well. What matters considerably more than which library a team chooses is the discipline covered in this guide's second half: verifying only genuinely meaningful behavioral contracts rather than over-specifying implementation detail, recognizing when a fake or a real Testcontainers-backed integration test would serve better than an elaborately configured mock, and — most importantly — treating an unusually heavy mocking burden as a signal worth investigating in the design of the class under test, not a problem to paper over with a bigger convenience tool.
This connects directly to the testing pyramid this series has built out across its xUnit, Integration Tests, and Testcontainers guides — mocking frameworks are precisely the tool for the pyramid's fast, numerous base layer, and understanding both their genuine power and their honest limits (sealed classes, static methods, the design-smell signal of excessive setup) is what keeps that base layer trustworthy rather than merely fast.
Found this useful? Feel free to star the repo, open an issue with corrections, or share the moment an unreasonably long mock setup finally convinced you to split up an overgrown class.
Top comments (0)