Skip to content

.NET Core · Testing

Mocking in .NET with Moq and NSubstitute

6 min read Updated 2026-06-23 Share:

Practice Mocking interview questions

Why mocking comes up in every .NET testing interview

Mocking is the technique that makes unit testing possible for real applications. Without it, testing a PaymentService would require a live payment gateway; testing an OrderRepository would require a database. Interviewers probe mocking to assess whether a candidate understands when to mock (not everything!), how to configure mocks correctly, and the difference between stubs and interaction verification.

What a mock is — and what it is not

A mock is a controllable substitute for a real dependency that:

  1. Returns predetermined values so you can test the unit in isolation
  2. Records method calls so you can verify interactions after the fact
// Real dependency — hits SMTP in every test:
public class OrderService
{
    private readonly SmtpEmailSender _emailer = new(); // tight coupling
    public void PlaceOrder(Order o) => _emailer.Send(o.Email, "Confirmed!");
}

// Mockable — dependency injected through an interface:
public class OrderService
{
    private readonly IEmailSender _emailer;
    public OrderService(IEmailSender emailer) => _emailer = emailer;
    public void PlaceOrder(Order o) => _emailer.Send(o.Email, "Confirmed!");
}

// In tests:
var mockEmailer = new Mock<IEmailSender>();
var service     = new OrderService(mockEmailer.Object);
service.PlaceOrder(new Order { Email = "a@b.com" });
mockEmailer.Verify(e => e.Send("a@b.com", "Confirmed!"), Times.Once);

The interface is the seam. Without it, there is nothing to replace.

The five types of test doubles

The word "mock" is used loosely, but there are actually five distinct types:

TypeDoesWhen to use
DummyPassed but never usedFill required constructor parameters
StubReturns fixed valuesControl return values without verifying calls
MockVerifies interactionsAssert that a side effect occurred
SpyRecords calls for later inspectionCapture arguments passed to a method
FakeWorking simplified implementationComplex interactions too hard to stub

In practice, Moq creates stubs and mocks (and spies via Callback). Fakes are usually hand-written.

Moq: the .NET mocking standard

Moq is the most widely used mocking library in .NET. The core API is three methods: Setup, Returns, and Verify.

Configuring return values

var mockRepo = new Mock<IProductRepository>();

// Return a specific value for exact arguments:
mockRepo.Setup(r => r.GetById(42)).Returns(new Product { Id = 42 });

// Return for any argument:
mockRepo.Setup(r => r.GetById(It.IsAny<int>()))
        .Returns(new Product { Id = 1 });

// Return based on the argument value:
mockRepo.Setup(r => r.GetById(It.Is<int>(id => id > 0)))
        .Returns<int>(id => new Product { Id = id });

// Throw an exception:
mockRepo.Setup(r => r.GetById(-1)).Throws<ArgumentOutOfRangeException>();

// Async — use ReturnsAsync:
mockRepo.Setup(r => r.GetByIdAsync(42)).ReturnsAsync(new Product { Id = 42 });

Verifying interactions

mockEmailer.Verify(e => e.Send("a@b.com", It.IsAny<string>()), Times.Once);
mockEmailer.Verify(e => e.Send(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
mockEmailer.VerifyNoOtherCalls(); // fail if any unexpected calls occurred

Capturing arguments with Callback

var savedOrders = new List<Order>();
mockRepo.Setup(r => r.Save(It.IsAny<Order>()))
        .Callback<Order>(o => savedOrders.Add(o))
        .Returns(true);

service.PlaceOrder(new Order { Sku = "X" });
service.PlaceOrder(new Order { Sku = "Y" });

Assert.Equal(2, savedOrders.Count);
Assert.Equal("X", savedOrders[0].Sku);

Callback is more powerful than Verify for asserting on complex argument state — it captures the full object for later inspection.

Sequential return values

// SetupSequence — different return per call:
mockHttp.SetupSequence(c => c.GetAsync("/api/data"))
        .Returns(Task.FromResult((string?)null))          // 1st call
        .Returns(Task.FromResult("{ \"ok\": true }"))     // 2nd call
        .Throws<HttpRequestException>();                   // 3rd call

This is essential for testing retry logic or stateful workflows.

Strict vs loose mocks

Loose (the default) returns default(T) for any unconfigured call. Strict throws MockException for any unconfigured call.

var looseMock  = new Mock<IProductRepository>();              // unconfigured → null
var strictMock = new Mock<IProductRepository>(MockBehavior.Strict); // unconfigured → exception

Most teams stay with loose mocks and use VerifyNoOtherCalls() when they need strict enforcement on a specific test. Global strict behaviour is brittle — adding a logging call to production code breaks every strict mock that didn't set it up.

Argument matchers

// Any value of the type:
mock.Setup(r => r.Save(It.IsAny<Order>())).Returns(true);

// Value satisfying a predicate:
mock.Setup(r => r.Save(It.Is<Order>(o => o.Total > 0))).Returns(true);

// Not null:
mock.Setup(r => r.Save(It.IsNotNull<Order>())).Returns(true);

// Value in a set:
mock.Setup(r => r.GetByStatus(It.IsIn(OrderStatus.Pending, OrderStatus.Processing)))
    .Returns(new List<Order>());

Constraint: if you use a matcher for one argument, all arguments in that call must use matchers too. You cannot mix It.IsAny<int>() with a bare literal.

NSubstitute: an alternative syntax

NSubstitute removes the lambda-wrapping and reads closer to plain C#:

// Moq:
var mock = new Mock<ICalculator>();
mock.Setup(c => c.Add(2, 3)).Returns(5);
mock.Verify(c => c.Add(2, 3), Times.Once);

// NSubstitute:
var sub = Substitute.For<ICalculator>();
sub.Add(2, 3).Returns(5);
sub.Received(1).Add(2, 3); // verify after the fact

Both integrate with xUnit and NUnit equally well. NSubstitute has no strict mode; use DidNotReceive() and ReceivedCalls() for inspection. The choice is mostly team preference.

The most common mocking mistake: over-mocking

Over-mocking means replacing real collaborators with mocks even when the real implementation is fast, deterministic, and side-effect-free. The result is tests that break on every refactor even when behavior is unchanged.

// Over-mocked — tests the call graph, not the behavior:
var mockValidator = new Mock<IOrderValidator>();
var mockPricer    = new Mock<IPricingEngine>();
var mockAudit     = new Mock<IAuditLogger>();
// ... configure all three ...
// ... verify all three were called ...

// Better — only mock what crosses a process boundary:
var mockEmailer = new Mock<IEmailSender>();  // side effect: real email
var mockRepo    = new Mock<IOrderRepository>(); // side effect: real DB

var service = new OrderService(
    new OrderValidator(),   // real — fast, no side effects
    mockRepo.Object,
    new PricingEngine(),    // real — pure calculation
    mockEmailer.Object);

A useful question: "If I replace this dependency with a mock, does the test run faster or more reliably?" If the answer is no — if the real implementation is just a pure function or a simple value object — use the real one.

Mocking HttpClient

HttpClient is a concrete class with no interface, but its HttpMessageHandler is virtual. Swap the handler:

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly HttpResponseMessage _response;
    public MockHttpMessageHandler(HttpResponseMessage r) => _response = r;

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage req, CancellationToken ct)
        => Task.FromResult(_response);
}

var handler    = new MockHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.OK)
    { Content = new StringContent("""{"id":1}""", Encoding.UTF8, "application/json") });
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://api.example.com") };
var service    = new ProductService(httpClient);

Always inject HttpClient or IHttpClientFactory — never new HttpClient() inside a class, which makes it impossible to test.

Read-only property mocking

Moq can mock get-only properties on interfaces or virtual properties on classes:

var mockCtx = new Mock<IUserContext>();
mockCtx.Setup(c => c.UserId).Returns("user-42");
mockCtx.Setup(c => c.IsAdmin).Returns(true);

If a property is concrete and non-virtual, you cannot mock it — extract an interface.

Rule of thumb: Mock collaborators at the boundary between your code and the outside world (databases, email, HTTP, time). Use real implementations for same-process logic. If a mock makes a test harder to read without making it faster or more reliable, the mock is doing more harm than good.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel