HT
How Things Work
System Online

Mocking & Dependency Isolation

Replacing real dependencies with test doubles — stubs, mocks, fakes, and spies; state-based vs interaction-based testing; and the discipline of mocking only what you don't own.

How It Works

Test doubles stand in for real dependencies so a unit test stays fast, isolated, and deterministic. Stubs feed canned data, mocks verify interactions, fakes are lightweight working implementations, and spies record usage. The art isn't the tooling (Moq, NSubstitute, jest.mock) — it's judgment: mock the boundaries you don't control, and use real objects or fakes for your own logic to avoid tests that merely echo the implementation.

1
Why Doubles Exist

A unit test should exercise one unit, fast and deterministically. Real dependencies — databases, HTTP APIs, the system clock, payment gateways — are slow, flaky, or have side effects. Test doubles replace them so the test isolates the unit's own logic and runs in milliseconds.

2
The Family of Doubles

Stubs return canned values to drive the test. Mocks are pre-programmed with expectations and verify interactions. Fakes are lightweight working implementations (an in-memory repository). Spies record how a real object was used so you can assert afterward. They're all 'test doubles'; the word 'mock' is often used loosely for all of them.

3
State-Based vs Interaction-Based

Two testing styles: state-based asserts on the result/state after acting (use stubs and fakes) — robust and refactor-friendly. Interaction-based asserts that the SUT called a collaborator correctly (use mocks/spies) — necessary when the behavior IS the call (e.g. 'sends an email'), but more coupled to implementation.

4
Mock Roles, Not Everything

The guideline: mock across architectural boundaries you don't own or can't control (external APIs, time, randomness, I/O), and use real objects or fakes for your own collaborating code. Over-mocking produces tests that just re-state the implementation and pass even when the logic is wrong.

Key Concepts

🥫Stub

Provides canned return values to drive the test. You assert on the SUT's output, not the stub.

🎭Mock

Pre-programmed with expectations; the test verifies the SUT called it correctly (args, count).

🪶Fake

A real but lightweight working implementation (e.g. in-memory DB) — usable, not production-grade.

🕵️Spy

Records how a (possibly real) object was used so you can assert invocations afterward.

⚖️State vs Interaction

Assert the resulting state (stubs/fakes) vs assert the calls made (mocks/spies).

🚧Don't Mock What You Own

Mock external boundaries (HTTP, time, APIs); use real objects/fakes for your own logic.

Stub vs mock (Moq & Jest)
tsx
1// STUB (state-based): canned answer, assert the SUT's output.
2var gateway = new Mock<IPaymentGateway>();
3gateway.Setup(g => g.Charge(It.IsAny<decimal>())).Returns(new Receipt(true));
4var sut = new OrderService(gateway.Object);
5sut.Place(order);
6order.Status.Should().Be(OrderStatus.Paid); // assert OUTPUT
7
8// MOCK (interaction-based): assert the dependency was called correctly.
9var mailer = new Mock<IMailer>();
10new OrderService(gateway.Object, mailer.Object).Place(order);
11mailer.Verify(m => m.Send(order.Email), Times.Once); // assert INTERACTION
12
13// jest equivalent:
14const charge = jest.fn().mockResolvedValue({ ok: true }); // stub
15expect(charge).toHaveBeenCalledWith(99.0); // mock-style verify
16
17// RULE OF THUMB: mock what you DON'T own or can't control
18// (HTTP, payment APIs, the clock). Use real objects / fakes for your own logic.
💡
Why This Matters

Mocking is essential for isolating units, and the stub/mock/fake/spy distinction plus 'don't over-mock' is a frequent interview and code-review topic. Used well, doubles make fast deterministic tests; used carelessly, they create brittle tests coupled to implementation that pass even when the code is broken.

Common Pitfalls

Over-mocking your own logic — tests end up verifying the mocks, not the behavior.
Interaction-based tests everywhere, coupling tests to implementation so refactors break them.
Mocking types you own when a real object or fake would be simpler and more truthful.
Stubs that drift from the real dependency's behavior, giving false confidence.
Confusing the terms (calling everything a 'mock') and reaching for the wrong double for the job.
Real-World Use Cases

1Testing Logic Without Charging Real Cards

Scenario

OrderService must be tested, but it calls a real payment gateway and sends real emails. Running the tests would hit external services, cost money, be slow, and fail whenever the gateway sandbox is down.

Problem

External side effects make the unit untestable in isolation — tests are slow, non-deterministic, and have real-world consequences (charges, emails), so the team avoids writing them.

Solution

Inject IPaymentGateway and IMailer interfaces and substitute test doubles: stub the gateway to return a successful receipt (then assert the order is marked Paid), and use a mock to verify the confirmation email was sent exactly once. The logic is fully tested with no external calls.

💡

Takeaway: Mock the boundaries you don't control (payments, email) so you can test your logic deterministically and for free. Inject dependencies as interfaces to create the seams that make this possible.

2Tests That Passed Even When the Code Was Wrong

Scenario

A service had thorough-looking tests, but a real bug shipped anyway. Investigation showed the tests mocked nearly every collaborator and mostly asserted that mocks were called — they never exercised the real logic.

Problem

Over-mocking turned the tests into a mirror of the implementation. They verified 'method A called method B' rather than 'given input X, the output is Y', so they couldn't detect incorrect logic — only structural changes.

Solution

Reduce mocking to genuine external boundaries, use real objects/fakes for in-process collaborators, and shift toward state-based assertions on actual outputs. Where interaction matters (an email IS sent), keep a focused mock — but stop mocking your own pure logic.

💡

Takeaway: Over-mocking creates tests that can't fail for the right reasons. Mock external boundaries, prefer real objects and state-based assertions for your own code, so tests verify behavior, not structure.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.