Testing Fundamentals — Vitest and Jest
IntermediateArrange, act, assert — the structure is the same as JUnit or pytest. What differs is how mocking works and how async assertions behave.
Overview
If you have written JUnit or pytest, the shape of a JavaScript test will be immediately recognisable: describe blocks, a setup hook, and assertions. Vitest and Jest share almost the same API, and Vitest is the faster default for anything built with Vite. The parts worth attention are async assertions, which fail silently if you forget to await, and mocking, which in the module-based JavaScript world means intercepting an import rather than injecting a dependency.
Structure and Assertions
The basic shape, and the matchers you will use constantly.
import { describe, it, expect, beforeEach, vi } from 'vitest'
describe('calculateReadiness', () => {
let attempts
beforeEach(() => { attempts = [] }) // fresh state per test
it('returns provisional below five attempts', () => {
const result = calculateReadiness(attempts) // act
expect(result.provisional).toBe(true) // assert
})
it.each([
[0, 'not started'],
[45, 'developing'],
[85, 'ready'],
])('labels %i as %s', (score, label) => {
expect(labelFor(score)).toBe(label)
})
})
// Matchers that matter
expect(x).toBe(1) // Object.is — primitives, identity
expect(obj).toEqual({ a: 1 }) // deep equality, ignores undefined keys
expect(obj).toStrictEqual({ a: 1 }) // deep, and does not ignore them
expect(arr).toHaveLength(3)
expect(arr).toContainEqual({ id: 1 })
expect(fn).toThrow(ValidationError)
expect(obj).toMatchObject({ id: 1 }) // subset matchAsync Tests
The one mistake everybody makes: a test that passes because it finished before the assertion ran.
// Broken — the test ends before the promise settles, and passes
it('fetches', () => {
getProblem('two-sum').then(p => expect(p.title).toBe('Wrong'))
})
// Correct
it('fetches', async () => {
const p = await getProblem('two-sum')
expect(p.title).toBe('Two Sum')
})
// Asserting a rejection — note the await on expect
await expect(getProblem('nope')).rejects.toThrow(NotFoundError)
await expect(getProblem('two-sum')).resolves.toMatchObject({ slug: 'two-sum' })
// Guard against a silent pass when a throw was expected
it('rejects invalid input', async () => {
expect.assertions(1) // fails if no assertion ran
await expect(parse('')).rejects.toThrow()
})
// Fake timers, for debounce and retry logic
vi.useFakeTimers()
debounced()
vi.advanceTimersByTime(300)
expect(fn).toHaveBeenCalledOnce()
vi.useRealTimers()Mocking
Module mocks, spies, and the argument for mocking the network rather than your own code.
// Spy on a function
const onSave = vi.fn()
onSave.mockReturnValue(true)
onSave.mockResolvedValue({ id: 1 })
expect(onSave).toHaveBeenCalledWith({ slug: 'two-sum' })
// Mock a whole module — hoisted above the imports
vi.mock('@/lib/api', () => ({
getProblem: vi.fn().mockResolvedValue({ slug: 'two-sum' }),
}))
// Usually better: intercept at the network layer with MSW, so the
// test exercises your real fetch code and only the server is fake
import { http, HttpResponse } from 'msw'
server.use(
http.get('/api/problems/:slug', () =>
HttpResponse.json({ slug: 'two-sum', title: 'Two Sum' })),
)
// Reset between tests or state leaks across them
afterEach(() => { vi.restoreAllMocks() })
// Coverage is a map of what is untested, not a score to chase.
// 100% coverage of getters proves nothing; the branches in your
// scoring logic are where the tests belong.Key Points to Remember
- 1toBe compares identity, toEqual compares deeply — using toBe on objects is the classic false failure
- 2An async test must await, or it passes without ever running the assertion
- 3Use rejects.toThrow with await, and expect.assertions(n) to guard against silently skipped assertions
- 4vi.mock replaces a module; MSW intercepts the network so your real fetch code is still exercised
- 5Coverage shows what is untested — chase meaningful branches, not the percentage
Interview Questions
Sign in to ask AriaWhat is the difference between toBe and toEqual?
Why can an async test pass even when its assertion would fail?
Why might you mock the network rather than mocking your own API module?
Ask Aria about Testing Fundamentals — Vitest and Jest
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.