Quiz

What are mocks and stubs and how are they used in testing?

Topics
JavaScriptTesting

TL;DR

A stub supplies controlled behavior, such as making a payment client return an approved response. A spy records calls. A mock often combines configured behavior with interaction expectations, although libraries use these terms differently. Use them at slow, nondeterministic, destructive, or unavailable boundaries—not to duplicate every implementation detail. Over-mocking can produce tests that pass while the real modules no longer work together.


Test doubles at a dependency boundary

Both replace a real collaborator, but a stub primarily supplies controlled answers while a mock also asserts how the collaborator was used.

Stubs and mocks around a system under test

Terminology varies among libraries, so the valuable distinction is whether the double controls indirect input, verifies interaction, or does both.

Stubs control indirect input

This service depends on a payment client. A stub makes the failure path deterministic without charging a card:

async function placeOrder(paymentClient, order) {
const payment = await paymentClient.charge(order.total);
if (!payment.approved) throw new Error('Payment declined');
return { orderId: order.id, paymentId: payment.id };
}
import { expect, test, vi } from 'vitest';
test('rejects a declined payment', async () => {
const paymentClient = {
charge: vi.fn().mockResolvedValue({ approved: false }),
};
await expect(
placeOrder(paymentClient, { id: 'order-1', total: 25 }),
).rejects.toThrow('Payment declined');
});

Here charge is acting primarily as a stub: its controlled response drives the code under test.

Spies and mocks verify indirect output

When the call itself is part of the contract, inspect it:

test('charges the order total once', async () => {
const charge = vi.fn().mockResolvedValue({ approved: true, id: 'payment-1' });
await placeOrder({ charge }, { id: 'order-1', total: 25 });
expect(charge).toHaveBeenCalledOnce();
expect(charge).toHaveBeenCalledWith(25);
});

This test uses the function as both a stub and a spy; it is commonly called a mock function. Sinon, Jest, and Vitest expose related APIs with different terminology.

When to use them

Good boundaries include payment/email providers, the clock, randomness, filesystem access, and failure responses that are hard to trigger reliably. Prefer a lightweight real implementation when it is fast and deterministic—for example, a real parser or in-memory data structure.

Avoid asserting every internal call sequence. Those tests become coupled to refactoring and can hide integration defects. Add contract or integration tests around important adapters so the fake behavior stays aligned with the real dependency.

Always restore patched globals and spies after the test. Test runners provide hooks such as afterEach() and restoration helpers for this cleanup.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which uses of test doubles are appropriate? Select all that apply.