Home/Learn/JavaScript & TypeScript/Testing Fundamentals — Vitest and Jest

Testing Fundamentals — Vitest and Jest

Intermediate
Quality

Arrange, 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.

describe/it, and toBe vs toEqual
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 match

Async Tests

The one mistake everybody makes: a test that passes because it finished before the assertion ran.

await the assertion, or it proves nothing
// 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.

vi.mock, spies, and MSW
// 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 Aria
1

What is the difference between toBe and toEqual?

Easy
2

Why can an async test pass even when its assertion would fail?

Medium
3

Why might you mock the network rather than mocking your own API module?

Hard

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.

Loading discussion…