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:
- Returns predetermined values so you can test the unit in isolation
- 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:
| Type | Does | When to use |
|---|---|---|
| Dummy | Passed but never used | Fill required constructor parameters |
| Stub | Returns fixed values | Control return values without verifying calls |
| Mock | Verifies interactions | Assert that a side effect occurred |
| Spy | Records calls for later inspection | Capture arguments passed to a method |
| Fake | Working simplified implementation | Complex 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.