Unit Test Mocking

Unit test mocking is the practice of replacing parts of a system under test with controlled, synthetic substitutes so that the test can verify the unit’s behavior without invoking external services, databases, or other units. It matters whenever a test’s speed, determinism, or isolation would be compromised by reaching beyond the unit’s direct boundaries—a database transaction, an HTTP endpoint, a file system write, or a clock-dependent calculation. Mocking is not about avoiding real integration tests; it is about enabling unit tests to run in milliseconds, repeatably, on a developer’s laptop or a CI runner, while keeping the integration suite as the source of truth for end-to-end behavior.

Isolate a unit from a slow or failing dependency

The most common reason to mock is to prevent a test from hitting a database, a network service, or a filesystem operation that is either too slow for a tight test loop or unreliable in a CI environment. The reader’s goal is to replace that dependency with an in-memory stand-in that returns data or errors on demand.

Python – unittest.mock.patch Python’s standard library provides unittest.mock.patch, which temporarily replaces a target attribute with a mock object. The patch is applied by importing the module where the dependency is *used*, not where it is defined, because Python resolves attributes at runtime.

from unittest import mock
import requests

def fetch_user(user_id):
    resp = requests.get(f"https://api.example.com/users/{user_id}")
    resp.raise_for_status()
    return resp.json()


def test_fetch_user_success():
    with mock.patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
        mock_get.return_value.raise_for_status.return_value = None
        result = fetch_user(1)
        assert result == {"id": 1, "name": "Alice"}
        mock_get.assert_called_once_with("https://api.example.com/users/1")

The patch target uses the dotted path where requests.get is *looked up* inside fetch_user’s module scope. The return_value chain configures what json() and raise_for_status() return without executing any real I/O.

Go – mockgen Go projects often generate mock implementations from interfaces using mockgen, a tool shipped with the go.mock tooling ecosystem. The generated file satisfies the interface contract while allowing the test to inject arbitrary returns.

# Generate a mock for the UserRepository interface
mockgen -source=user_repository.go -destination=user_repository_mock.go

The produced user_repository_mock.go contains a MockUserRepository struct with methods matching every function in the interface, each returning nil values by default. The test can then set expectations:

import (
    "testing"
    "github.com/golang/mock/gomock"
    "myproject/user"
)

func TestCreateUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    mockRepo := user.NewMockUserRepository(ctrl)

    mockRepo.EXPECT().Create(gomock.Any()).Return(nil).Times(1)

    svc := userService{repo: mockRepo}
    svc.CreateUser(context.Background(), "Alice")
}

The EXPECT() chain configures exactly one call to Create, with any argument, returning nil. If the code under test calls Create zero times or more than once, the test fails immediately with a clear “unexpected call” message.

JavaScript/Node – jest.mock and jest.fn() Jest provides jest.mock to replace an entire module with a manual mock, and jest.fn() to create individual test doubles inline.

// utils/api.js
export async function fetchUser(id) {
  const resp = await fetch(`https://api.example.com/users/${id}`);
  if (!resp.ok) throw new Error("Network error");
  return resp.json();
}

// user.test.js
jest.mock("../utils/api"); // replaces the module with a __mocks__/api.js stub

test("fetchUser resolves with parsed JSON", async () => {
  const mockData = { id: 1, name: "Bob" };
  const fn = require("../utils/api").fetchUser;
  fn.mockResolvedValue(mockData);

  const result = await fn(1);
  expect(result).toEqual(mockData);
});

The manual mock file .__mocks__/api.js lives beside the module and exports a fetchUser function that returns whatever the test configures. This keeps the test completely offline.

Simulate external failure modes without side effects

Tests must also verify that a unit handles errors, timeouts, or malformed responses gracefully. Mocking lets the author inject those conditions without triggering real failure cascades.

SQL-driven units – parametrized result sets When a unit queries a database, the test can substitute a lightweight in-memory database or a result-set mock that returns predefined rows. A common pattern in Go uses testcontainers‑style approaches replaced by a sqlmock package that intercepts database/sql queries.

// sqlmock example (real package: github.com/DATA-DOG/go-sqlmock)
db, mock := sql.Open("postgres", "dbname=test")
defer db.Close()

mock.ExpectQuery("SELECT id, name FROM users WHERE id = $1").
    WithArgs(42).
    WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(42, "Alice"))

rows, err := db.Query("SELECT id, name FROM users WHERE id = $1", 42)

The mock records the exact SQL statement and parameters, and returns a row set that the code under test processes identically to a live PostgreSQL connection. If the query text or argument count diverges, the mock’s AssertExpectations fails with a diffed list of unmet expectations.

HTTP error injection – nock (Node) or responses (Python) For units that call HTTP endpoints, nock (Node) and responses (Python) intercept outbound requests at the socket level and return synthetic responses.

# responses example (real library: responses)
import responses
import requests

@responses.activate
def test_user_not_found():
    responses.add(
        responses.GET,
        "https://api.example.com/users/999",
        status=404,
        json={"error": "not found"}
    )
    try:
        requests.get("https://api.example.com/users/999")
        assert False, "Expected exception"
    except requests.HTTPError as e:
        assert e.response.status_code == 404

The interceptor matches the request URL and method, then returns the configured status and body. After the test, responses asserts that no unexpected requests were made, surfacing accidental integration attempts.

Choose the right kind of test double

Readers often confuse mocks, stubs, and fakes. Organizing by intent clarifies which tool fits the goal.

  • Stub provides canned data to answer calls without asserting how the unit used the data. Use a stub when the test only needs the unit to proceed past a dependency.
  • Mock records expectations and verifies that the unit interacted with the dependency in a specific way. Use a mock when the test must confirm that a particular method was called with particular arguments, or that a side effect (e.g., a log entry, a cache write) occurred.
  • Fake is a fully functional in-memory replacement that mimics the real thing’s behavior but uses simplified algorithms or storage. A fake SQL database that stores rows in memory is a fake. Use a fake when the unit’s logic depends on real behavior (e.g., ordering, aggregation) that a stub cannot provide, but a live database is too heavy.

A test that only asserts assert_called_once() uses a mock. A test that calls service.process(order) and then asserts the order appeared in a report uses a fake or integration test, depending on whether the report writer is also under test.

Sharp edges – what silently fails and why

Even with real tools, mocking introduces failure modes that catch teams off guard.

State leakage between tests Mock objects often persist across test functions if teardown is incomplete. In Python, patch stops at the with block’s exit, but if a module-level patch is applied at import time, subsequent tests may see the mock still active. In Go, gomock controllers must be deferred Finish(); forgetting this leaves expectations registered for the next test, producing spurious “unexpected call” failures. Always pair mock setup with scoped context managers or explicit controller cleanup.

Over-mocking and fragile tests When a test mocks every collaborator, it stops testing the unit’s behavior and starts testing the test author’s assumptions about how the unit *should* call its dependencies. A change in the unit’s internal flow that uses a different method signature will break the test, even though the unit’s public behavior remains correct. Limit mocks to the boundary of the unit under test; let lower-level collaborators be real stubs or fakes unless their behavior must be verified.

Ordering and sequence expectations Some mock frameworks let you assert call order (Expect().Times(1).After(expect2)). This makes tests fragile: refactoring the unit’s method call sequence rewrites the test, even if the end result is identical. Prefer counting calls with Times(n) or AtLeast(n) over strict ordering, unless the unit’s correctness hinges on exact interleaving (e.g., a double-checked lock pattern).

Mock vs stub vs fake in CI A mock that works on a developer’s machine may behave differently on a CI runner if the mock’s implementation relies on unmocked environment variables, time functions, or random values. A fake that uses time.Now() will produce non-deterministic results unless the test freezes time (freeze_time in Python, clockwork in Go). Always inspect what the mock or fake captures from the surrounding environment, and explicitize those dependencies in the test setup.

Quick checklist for writing mock‑aware unit tests

  1. Identify the unit’s direct dependencies – only those that cross the unit’s public boundary need substitution.
  2. Pick the double by intent – stub if you only need data, mock if you need interaction verification, fake if you need realistic behavior without the real cost.
  3. Scope the double – use language‑scoped constructs (patch/with in Python, controller Finish() in Go, jest.mock at the top of the block) so tests cannot leak state.
  4. Configure returns/expectations explicitly – avoid “magic” defaults; write out what the stub returns, what the mock expects, what the fake stores.
  5. Assert after the unit runs – verify that the unit either used the double as expected (mock) or produced the correct output despite the double (stub/fake). Never assert on the double’s internal state unless that state is the unit’s responsibility.
  6. Run the integration suite – mocks make unit tests fast and isolated; they do not replace the integration test suite that exercises the real dependencies end‑to‑end.

Unit test mocking, when kept focused and scoped, is a disciplined way to keep the unit test suite as a fast, trustworthy safety net. The key is to let the test’s *intent* drive the choice of double, and to treat the mock as a temporary, verifiable boundary—not a substitute for real integration coverage.

This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.