Menu

Topic 3 of 8

Mocking Dependencies

Learn Mocking Dependencies for free with explanations, exercises, and a quick test (for API Engineer).

Published: January 21, 2026 | Updated: January 21, 2026

Why this matters

As an API Engineer, your services talk to databases, caches, message queues, payment gateways, and other microservices. If your unit tests hit real dependencies, they become slow, flaky, and hard to debug. Mocking dependencies lets you:

  • Isolate a unit of code and get instant feedback.
  • Simulate hard-to-reproduce scenarios: timeouts, retries, 500s, malformed payloads.
  • Test error handling and logging without impacting real systems.
  • Ship changes with confidence and maintain a stable CI pipeline.

Concept explained simply

Mocking replaces real collaborators with controlled stand-ins.

  • Stub: returns canned values. No behavior verification.
  • Mock: programmable stub that can also verify interactions (calls, arguments, count).
  • Fake: lightweight working implementation (e.g., in-memory repository).
  • Spy: records what happened; assertions can be made afterward.

State vs behavior:

  • State-based testing: assert outputs/returned values.
  • Behavior-based testing: assert that certain calls happened (e.g., retry method called 3 times).

Mental model

Think of your system as concentric circles:

  • Core logic (pure, no I/O) — test directly, no mocks.
  • Boundary adapters (HTTP, DB, queues, time, random) — mock or use fakes in unit tests.
  • Integration acceptance — confirm wiring with a few higher-level tests.

Trade-offs triangle: isolation, determinism, latency. Mocking optimizes isolation and determinism for fast feedback.

When not to mock
  • Do not mock the module you are testing (SUT). Mock its dependencies.
  • Prefer real code for simple value objects or pure functions.
  • Use integration/contract tests at service boundaries to prevent drift from reality.

Worked examples

Example 1 — Python: Mock an HTTP call and a retry
# payment.py
import requests

def charge(card_token, amount, http=requests):
    for attempt in range(3):
        try:
            resp = http.post("https://pay.example/charge", json={"card": card_token, "amount": amount}, timeout=2)
            if resp.status_code == 200 and resp.json().get("ok"):
                return {"status": "ok"}
        except requests.exceptions.Timeout:
            pass
    return {"status": "failed"}

# test_payment.py
from unittest import mock
import payment

def test_charge_retries_then_fails():
    http = mock.Mock()
    # First two attempts timeout, third returns 500
    http.post.side_effect = [requests.exceptions.Timeout(), requests.exceptions.Timeout(), mock.Mock(status_code=500, json=lambda: {"ok": False})]
    result = payment.charge("tok_123", 5000, http=http)
    assert result == {"status": "failed"}
    assert http.post.call_count == 3
Example 2 — Node.js (Jest): Mock Redis and the clock
// cache.js
module.exports = function makeCache(client, clock = { now: () => Date.now() }) {
  return {
    setWithTTL: async (key, value, ttlMs) => {
      const expiresAt = clock.now() + ttlMs;
      await client.set(key, JSON.stringify({ value, expiresAt }));
    },
    isExpired: async (key) => {
      const raw = await client.get(key);
      if (!raw) return true;
      const { expiresAt } = JSON.parse(raw);
      return clock.now() >= expiresAt;
    }
  };
};

// cache.test.js
const makeCache = require('./cache');

test('sets TTL using injected clock', async () => {
  const client = { set: jest.fn().mockResolvedValue('OK'), get: jest.fn() };
  const clock = { now: () => 1_000 }; // fixed time
  const cache = makeCache(client, clock);
  await cache.setWithTTL('k', 'v', 500);
  expect(client.set).toHaveBeenCalledTimes(1);
  const payload = JSON.parse(client.set.mock.calls[0][1]);
  expect(payload.expiresAt).toBe(1_500);
});
Example 3 — Java (Mockito): Mock repository and capture arguments
// UserService.java
public class UserService {
  private final UserRepo repo;
  private final Mailer mailer;
  public UserService(UserRepo repo, Mailer mailer) { this.repo = repo; this.mailer = mailer; }
  public void register(String email) {
    if (repo.exists(email)) throw new IllegalStateException("taken");
    User u = new User(email);
    repo.save(u);
    mailer.sendWelcome(email);
  }
}

// UserServiceTest.java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
  @Mock UserRepo repo;
  @Mock Mailer mailer;
  @InjectMocks UserService service;

  @Test void saves_and_sends_mail() {
    when(repo.exists("a@b.com")).thenReturn(false);
    service.register("a@b.com");
    ArgumentCaptor cap = ArgumentCaptor.forClass(User.class);
    verify(repo).save(cap.capture());
    assertEquals("a@b.com", cap.getValue().getEmail());
    verify(mailer).sendWelcome("a@b.com");
    verifyNoMoreInteractions(repo, mailer);
  }
}

Setup patterns that make mocking easy

  • Dependency injection: accept adapters/clients via constructor or function parameter.
  • Wrap globals: create tiny interfaces for time, uuid, random, filesystem; inject them.
  • Adapter pattern: hide vendor SDKs behind your interface to limit mocking surface.
  • Deterministic inputs: seed random; inject fixed clocks; freeze UUID for tests.
  • Use fakes for complex state: in-memory repo or fake queue for unit tests.
Smells and anti-patterns
  • Over-mocking: asserting every tiny call. Prefer asserting outcomes at boundaries.
  • Mocking what you own internally: test real collaborators if they are fast and pure.
  • Leaky tests: duplicating production logic in mocks.
  • Flaky timers: not controlling timeouts/sleeps.

Step-by-step: adding mocks to legacy code

  1. Identify the seam: where the function talks to I/O (HTTP, DB, time).
  2. Extract an interface or parameter for that collaborator.
  3. Pass the real dependency in production; pass a mock/fake in tests.
  4. Cover happy path with a stubbed success.
  5. Add error and timeout scenarios; verify retries/logging as needed.
  6. Refactor to reduce behavior verification if state assertions suffice.

Exercises (do these now)

Mirror of the exercises below. Aim for fast, deterministic tests. After completing, check off:

  • Created mocks/stubs without touching real network or disk.
  • Verified at least one behavior (call count or args) only where meaningful.
  • Used dependency injection (parameters or constructors).
  • Controlled time or randomness for determinism.
  • Wrote one failing test first, then made it pass.
Exercise 1 — Node.js/Jest: Stub HTTP client
Goal: Your function fetches user data and maps it to an internal DTO. Stub the HTTP client to return fixed JSON and assert mapping.
// userClient.js
async function getUser(http, id) {
  const res = await http.get(`/users/${id}`);
  const { id: userId, email, name } = res.data;
  return { id: userId, email, displayName: name };
}
module.exports = { getUser };

Write a Jest test that stubs http.get to resolve with { data: { id: 7, email: "x@y.com", name: "A B" } } and asserts the DTO is { id: 7, email: "x@y.com", displayName: "A B" }.

Exercise 2 — Python: Mock time and random
Goal: Make ID generation deterministic and test expiry checks.
# token.py
import random, time

def issue_token(rng=random, clock=time):
    tid = f"t_{rng.randint(100,999)}"
    return {"id": tid, "exp": int(clock.time()) + 60}

Write a test that injects fakes: rng with randint() returning 123, and clock with time() returning 1000. Assert id == "t_123" and exp == 1060.

Exercise 3 — Java/Mockito: Verify interactions
Goal: Service should not save when user exists; it should log and return.
// Service
public boolean ensureUser(String email, UserRepo repo, Logger log) {
  if (repo.exists(email)) { log.info("exists"); return false; }
  repo.save(new User(email));
  log.info("created");
  return true;
}

Write a test that stubs repo.exists to true, verifies save is never called, and log.info("exists") is called once.

Common mistakes and self-check

  • Mocking the system under test instead of its dependencies.
  • Hardcoding time/randomness causing flaky tests.
  • Asserting every internal call (brittle). Prefer outcome assertions.
  • Leaving network calls in unit tests.
  • Mocks returning unrealistic data (drift). Use representative payloads.
Self-check questions
  • Can this test fail without any code change? If yes, what non-determinism remains?
  • Is there a simpler state assertion instead of verifying calls?
  • Does the mock reflect a plausible real-world response?
  • Are integration tests covering the real boundary elsewhere?

Practical projects

  • HTTP adapter: Build a small client for a public-style API (e.g., payments). Unit-test it with mocked HTTP for success, 4xx, 5xx, and timeouts.
  • Clock + UUID wrapper: Create interfaces for time and id generation; refactor a feature to inject them and make tests deterministic.
  • In-memory fake repo: Replace DB calls with a fake for unit tests; keep one integration test with a real DB or container.
  • Retry + backoff: Implement retry logic; test with mocks verifying retry count and jitter via injected RNG.

Who this is for

  • API Engineers building services that call external systems.
  • Backend devs who want fast, reliable unit tests.
  • Engineers improving CI stability and coverage.

Prerequisites

  • Comfortable writing unit tests in at least one language (Python, Node.js, or Java).
  • Basic understanding of HTTP and I/O operations.
  • Familiarity with dependency injection patterns.

Learning path

  1. Unit testing fundamentals (assertions, test structure).
  2. Mocking dependencies (this lesson).
  3. Contract tests for service boundaries.
  4. Integration tests with real components where needed.
  5. Observability: logs and metrics assertions where appropriate.

Next steps

  • Refactor two existing tests to remove real network calls.
  • Introduce clock/uuid wrappers in your codebase.
  • Add at least one contract test to validate real payloads.

Mini challenge

Your endpoint uses an email provider SDK. Write a unit test that:

  • Injects the email client as a dependency.
  • Stubs a transient failure on first call and success on second.
  • Asserts your service retries once and returns a success result.

Tip: Use a mock with side effects to simulate the failure then success.

Quick Test is available below for everyone. Only logged-in users will see saved progress.

Practice Exercises

3 exercises to complete

Instructions

Your function maps remote user data to an internal DTO.

// userClient.js
async function getUser(http, id) {
  const res = await http.get(`/users/${id}`);
  const { id: userId, email, name } = res.data;
  return { id: userId, email, displayName: name };
}
module.exports = { getUser };
  1. Create a fake http with get mocked to resolve to { data: { id: 7, email: "x@y.com", name: "A B" } }.
  2. Call getUser(fakeHttp, 7).
  3. Expect the result to equal { id: 7, email: "x@y.com", displayName: "A B" }.
  4. Optionally, assert http.get was called once with "/users/7".
Expected Output
{"id":7,"email":"x@y.com","displayName":"A B"}

Mocking Dependencies — Quick Test

Test your knowledge with 10 questions. Pass with 70% or higher.

10 questions70% to pass

Have questions about Mocking Dependencies?

AI Assistant

Ask questions about this tool