Quiz

How do you write unit tests for JavaScript code?

Topics
JavaScriptTesting

TL;DR

Choose a small behavioral unit, provide controlled inputs and dependencies, execute it, and assert its observable result or side effect. Cover representative success, boundary, and failure cases. Keep tests deterministic and independent, but do not mock every collaborator merely to make a test “unit sized”—an integration test may give more confidence when several modules form one behavior.

Vitest, Jest, Mocha with an assertion library, and Node.js's built-in test runner are current options. The runner matters less than tests that explain the contract and fail for meaningful regressions.


Start from a behavior

Consider a shipping-price rule rather than a trivial implementation detail:

// shipping.js
export function calculateShipping({ subtotal, isMember }) {
if (!Number.isFinite(subtotal) || subtotal < 0) {
throw new RangeError('subtotal must be a non-negative number');
}
if (isMember || subtotal >= 50) return 0;
return 5;
}

Using Vitest:

import { describe, expect, test } from 'vitest';
import { calculateShipping } from './shipping.js';
describe('calculateShipping', () => {
test.each([
[{ subtotal: 49, isMember: false }, 5],
[{ subtotal: 50, isMember: false }, 0],
[{ subtotal: 10, isMember: true }, 0],
])('returns the expected price for %o', (input, expected) => {
expect(calculateShipping(input)).toBe(expected);
});
test('rejects a negative subtotal', () => {
expect(() => calculateShipping({ subtotal: -1, isMember: false })).toThrow(
RangeError,
);
});
});

The cases document a normal price, the exact free-shipping boundary, the member rule, and invalid input. If the implementation changes from if statements to a lookup table, the tests still describe the same contract.

Isolate unstable boundaries deliberately

For a unit that sends email or charges a card, inject a controlled adapter instead of making a real remote request:

export function createWelcomeService({ emailClient }) {
return async function welcome(user) {
await emailClient.send({ to: user.email, template: 'welcome' });
return { welcomed: true };
};
}
import { expect, test, vi } from 'vitest';
import { createWelcomeService } from './welcome-service.js';
test('sends the welcome template to the new user', async () => {
const emailClient = { send: vi.fn().mockResolvedValue(undefined) };
const welcome = createWelcomeService({ emailClient });
await expect(welcome({ email: 'avery@example.com' })).resolves.toEqual({
welcomed: true,
});
expect(emailClient.send).toHaveBeenCalledWith({
to: 'avery@example.com',
template: 'welcome',
});
});

This verifies an externally meaningful side effect without sending an email. It does not assert private helper calls or internal variable values.

Practical checklist

  • Name the behavior and expected outcome, not the implementation method.
  • Use explicit expected values; avoid recomputing the expected result with the same logic as the production code.
  • Include important boundaries and error behavior, not every possible input.
  • Reset mutated globals, fake timers, DOM state, and mocks after each test.
  • Avoid real clocks, random values, networks, and shared databases in unit tests unless controlled.
  • Prefer an integration or end-to-end test when the risk lies in wiring, rendering, serialization, or a real platform boundary.
  • Treat coverage as a map of unexecuted code, not proof that assertions are useful.

Further reading

Exercícios

Verifique seu entendimento
Beta
Verifique seu entendimento Exercício
Verifique seu entendimento Exercício

A pure calculateShipping(weight, destination) function can return a price or throw for unsupported input. Outline a focused unit-test set and explain what should remain observable rather than implementation-coupled.