Unit / JS · Junior & Senior

Jest & Vitest

The dominant JavaScript and TypeScript unit testing frameworks — Jest for established React and Node projects, Vitest for modern Vite-based apps. Near-identical APIs, different performance characteristics.

Junior Senior
01

The Hook: Why Jest/Vitest?

Imagine building a car. You wouldn't wait until the entire vehicle is assembled to see if the headlights turn on. You'd test the bulb before it ever goes into the socket. In software, Jest and Vitest are your bulb testers.

Unit tests are the fastest, cheapest, and most reliable signal in your quality suite. While a Playwright test might take 10 seconds to log in and check a button, a Vitest test can check 100 different logic combinations in 10 milliseconds.

02

The Rule: The 1-1-1 Principle

A unit test follows the 1-1-1 rule to remain effective:

  • 1 Unit: Test exactly one function, component, or class in isolation.
  • 1 Path: Test one specific logical branch (e.g., "Success" is one test, "Error" is another).
  • 1 Reason to Fail: If the test fails, it should tell you exactly where the code is broken without any investigation.
03

The Analogy: Screws vs. Assemblies

Think of Unit Tests as checking individual screws for the right thread. Integration Tests are checking if two boards can be screwed together. End-to-End Tests are checking if the finished table can hold the weight of a feast.

If you don't test the screws (Unit), you'll spend hours wondering why the table (E2E) is wobbly, only to find a single stripped thread at the very bottom.

Senior engineer insight

The moment your team starts writing tests for coverage rather than confidence, you have lost the plot. I have seen NZ fintech teams hit 90% line coverage and still ship a broken GST rounding bug in production — because every test asserted the happy path and nothing else. Code coverage measures which lines were executed, not whether you tested the right behaviour.

Pick Jest when your project already uses Babel, Create React App, or a mature Node back-end — the ecosystem tooling (snapshot serialisers, module transforms, jsdom) is battle-tested. Pick Vitest the moment you see Vite in the stack; running the tests through the same transform pipeline as the app eliminates an entire class of "it works in tests but breaks in the browser" surprises.

The most common mistake: teams mock everything and end up with tests that pass even after completely deleting the code under test — the mock was doing all the work.

04

The Setup: Getting Ready

For modern projects using Vite, we use Vitest. It's built for speed and requires almost zero configuration.

npm install -D vitest

In your package.json, add this script:

"scripts": {
  "test": "vitest"
}
05

The First Script: Hello Logic

Create a file named math.test.ts. Notice the .test.ts suffix — this is how the runner finds your tests.

import { expect, test } from 'vitest'

function add(a, b) {
  return a + b
}

test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3)
})

Run it with npm test. You'll see a green "PASS" in your terminal in a fraction of a second.

06

The "Gotcha": The Mocking Trap

The most common mistake junior testers make is testing external dependencies (like an API) in a unit test. If your unit test calls a real database, it's no longer a unit test — it's an integration test.

The Fix: Use Mocks. If your function calls fetch(), you "mock" that fetch to return a fake JSON response instantly. This ensures your test is only measuring your code, not the network's reliability.

From the field

A Wellington-based government digital services team migrated a legacy Node.js payments gateway to a modern Vite/React front-end. They ported their entire Jest suite across and everything went green in CI. Two sprints later, date formatting on payment receipts broke silently in production — the Jest config was using a UTC timezone in the test environment while Vitest picked up the system locale from the developer machines, and the two environments were producing different outputs for the same function.

The team had assumed "same API, safe to migrate" — and that is almost true, but environment configuration, module resolution order, and fake timer behaviour have meaningful differences between Jest and Vitest that only surface at the edges. The fix was a vitest.config.ts that explicitly pins timezone and locale, and a smoke test that checks a known date format string. The lesson that generalises: treat a Jest-to-Vitest migration as a configuration change that needs its own verification pass, not just a find-and-replace on import statements.

07

The Framework: AAA Pattern

Professional unit tests follow the AAA Pattern:

  • Arrange: Set up the data and conditions (e.g., create a fake user).
  • Act: Call the function you are testing.
  • Assert: Check that the result matches your expectation.
test('calculates discount correctly', () => {
  // Arrange
  const price = 100
  const discount = 0.2
  
  // Act
  const result = calculateTotal(price, discount)
  
  // Assert
  expect(result).toBe(80)
})
08

The NZ Example: GST Calculator

Let's test a real New Zealand business requirement: calculating GST. In NZ, GST is 15%.

// gst.ts
export function calculateGst(amount: number): number {
  return amount * 0.15
}

// gst.test.ts
import { calculateGst } from './gst'

test('calculates 15% GST on $100', () => {
  expect(calculateGst(100)).toBe(15)
})

test('handles zero amount', () => {
  expect(calculateGst(0)).toBe(0)
})

test('rounds to 2 decimal places', () => {
  // This test will fail if your function doesn't round!
  expect(calculateGst(10.55)).toBe(1.58) 
})
09

The Pro Move: Coverage Thresholds

Don't just run tests; measure what you've missed. Use the --coverage flag to see exactly which lines of code aren't tested.

Senior Strategy: In professional NZ teams, we often set "Coverage Thresholds" in the CI pipeline. If your new code drops the total project coverage below 80%, the build fails. This forces quality to be a continuous habit, not an afterthought.

Why teams fail here

  • Mock leakage between tests: Forgetting jest.clearAllMocks() or vi.restoreAllMocks() in afterEach means a mock set up in test 3 silently affects test 7 — the failures are random and order-dependent, which makes them nearly impossible to reproduce locally.
  • Testing implementation, not behaviour: Asserting that an internal helper function was called with specific arguments ties your tests to the implementation details. Refactor the internals and dozens of tests break even though the public behaviour is identical — this is the #1 reason teams stop trusting their test suite.
  • Async tests with missing await: A test that forgets await on a Promise will always pass because the assertion runs before the async operation completes. Jest and Vitest will not warn you by default — the test just silently asserts nothing.
  • Over-relying on snapshots: Snapshot tests seem like a free coverage boost but they degrade into noise. When a developer runs --updateSnapshot to "fix" a failing test without reading the diff, the snapshot becomes a lie that documents the current bug rather than the intended behaviour.

Key takeaway

A unit test suite that developers trust enough to act on is worth ten times more than a suite with high coverage that nobody believes — write tests that fail for exactly one reason, and you will always know where to look.

10

The Challenge: Your Turn

Write a function that validates a New Zealand Postcode. It must be exactly 4 digits and numeric.

Your Task: Write four tests: one for a valid postcode (e.g., 6011), one for 3 digits, one for 5 digits, and one containing letters. Use expect().toBe(false) for the invalid ones.

← All tools JUnit & TestNG →