---
title: "15 — Testing — Unit, Integration, and End-to-End in Node.js"
uid: testing
tags: ["vitest", "jest", "playwright", "node-test", "testing", "nodejs", "roadmap:nodejs", "cypress", "e2e"]
excerpt: "Tests live on a spectrum — unit (isolated) to integration to end-to-end (real browser) — and the tools split cleanly along it: Vitest/Jest/node:test on one side, Cypress/Playwright on the other."
date: 2026-08-13T03:27:54+0000
source: https://www.aveshina.my.id/en/blog/testing
---

"Write a test, run the test" was my testing model, and it skipped the decision that matters most: what kind of test am I writing. The model that finally stuck is a spectrum: **tests range from unit (one function, fully isolated) to integration (several units together) to end-to-end (a real browser driving the whole app), and the tools split cleanly along that line.** Vitest, Jest, and node:test cover the left side; Cypress and Playwright cover the right [1]. Knowing which kind of test I am writing is most of the decision.

The framing that landed for me is the spectrum, then the tools that map to each region.

## The testing spectrum

```figure
<svg viewBox="0 0 740 220" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Testing spectrum from unit to end-to-end. Left: a single function in isolation, fast, many tests. Middle: several units wired together with real dependencies. Right: a browser driving the whole application as a user would. Confidence rises left to right; speed and count fall.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- spectrum bar -->
    <rect x="40" y="60" width="660" height="36" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="82" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Unit</text>
    <text x="370" y="82" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">Integration</text>
    <text x="620" y="82" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">End-to-End</text>

    <!-- arrows under -->
    <text x="40" y="116" font-size="10" font-style="italic" fill="#64748b">fast, isolated, many</text>
    <text x="700" y="116" font-size="10" font-style="italic" fill="#64748b" text-anchor="end">slow, whole-app, few</text>
    <line x1="60" y1="126" x2="680" y2="126" stroke="#64748b" stroke-width="1.5"/>
    <polygon points="680,126 674,123 674,129" fill="#64748b"/>
    <text x="370" y="146" font-size="10" fill="#64748b" text-anchor="middle">confidence in the whole system rises →</text>

    <!-- tool clusters -->
    <rect x="50" y="168" width="240" height="36" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="170" y="190" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">Vitest  ·  Jest  ·  node:test</text>

    <rect x="450" y="168" width="240" height="36" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="570" y="190" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">Cypress  ·  Playwright</text>
  </g>
</svg>
```

The axis is confidence versus cost. A unit test is fast and pinpoints failures but proves only that one function works in isolation. An E2E test is slow and harder to debug but proves the whole system works together as a user experiences it. Real applications need both, weighted toward the unit end — the classic "testing pyramid" recommends many unit tests, fewer integration tests, and a small number of E2E tests [1].

## Unit and integration: Vitest, Jest, node:test

Three tools cover the left and middle of the spectrum, and they are more similar than different. Each provides a way to write a test, run it, and report pass/fail.

**Jest** was the long-standing default — a delightful, everything-included framework with describe/it/expect, mocks, and snapshot testing [2]. It works with Babel, TypeScript, React, Vue, and more, and an enormous amount of existing code uses it.

**Vitest** is the modern, Vite-native successor [3]. It is Jest-compatible (the same describe/it/expect API, so tests port with minimal changes), but it runs on Vite, which means native ESM and TypeScript support out of the box, faster startup, and hot-module-aware watching. For new projects — especially Vite-based ones — Vitest is now the default I reach for.

```
import { describe, it, expect } from 'vitest';
import { add } from './math';

describe('add', () => {
  it('sums two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });
});
```

**node:test** is Node's built-in test runner, added in Node 18 [4]. It has no dependencies, ships with Node, and provides the same describe/it/expect-style API. For a project that wants zero test-tooling dependencies — a library, a CLI, anything that values a minimal install — node:test is the practical choice.

```
import { test } from 'node:test';
import assert from 'node:assert/strict';

test('add sums two numbers', () => {
  assert.equal(add(2, 3), 5);
});
```

The three are interchangeable enough that the choice is mostly about ecosystem fit: Jest for existing codebases, Vitest for new Vite-based projects, node:test for zero-dependency minimalism. The test *shape* — small, fast, isolated — is the same in all three.

## End-to-end: Cypress and Playwright

The right end of the spectrum needs a different kind of tool, because the test has to drive a real browser the way a user would — click buttons, fill forms, read the rendered DOM, and assert on it. **Cypress** and **Playwright** are the two dominant options [5][6].

**Cypress** runs tests directly in the browser and ships a visual test runner that shows each command and the DOM state at every step, which makes debugging E2E tests genuinely pleasant. It is primarily oriented toward testing web applications in the browser.

**Playwright**, from Microsoft, supports multiple browsers (Chromium, Firefox, WebKit) and lets me write tests that simulate real user behavior across them [6]. It is faster for large suites and handles modern app patterns (network interception, multi-tab, file downloads) cleanly. Playwright can also be used for scraping and automating browser workflows, not only testing.

```
import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name=email]', 'ave@example.com');
  await page.fill('[name=password]', 'secret');
  await page.click('button[type=submit]');
  await expect(page).toHaveURL('/dashboard');
});
```

Both tools serve the same purpose — prove the whole system works as a user experiences it. Playwright's cross-browser coverage and speed make it the more common pick for new projects; Cypress's dev experience and runner make it a strong choice for teams that value the visual debug loop.

## The shape of a test, regardless of tool

Underneath the tool choice, every test has the same three-part shape — arrange, act, assert [1]:

1. **Arrange** — set up the preconditions (inputs, mocks, a fresh database state).
2. **Act** — call the thing under test.
3. **Assert** — verify the result matches the expectation.

Unit tests make "arrange" trivial (a few variables) and "assert" precise (one return value). E2E tests make "arrange" heavy (a running app, a seeded database, a browser session) and "assert" coarse (the page shows the right thing). Recognizing that the core shape is identical — and only the weight of each part changes — is what keeps the spectrum legible.

## How I use this

The model I keep is the spectrum, and it drives where I invest test effort. I write many unit tests with Vitest (or node:test for library work) — they are fast, they pinpoint failures, and they document how each function behaves. I write a smaller number of integration tests for the seams that unit tests miss (a function plus its real database, an API handler end to end). I write a small, curated set of E2E tests with Playwright covering the few critical user flows — login, checkout, the one path that must never break. The discipline is keeping the pyramid weighted toward the fast, isolated end: a suite of a thousand unit tests that runs in two seconds is worth more than a hundred E2E tests that take twenty minutes. The spectrum framing keeps the investment deliberate.

## References

[1] "Software testing," Wikipedia. [Online]. Available: [https://en.wikipedia.org/wiki/Software_testing](https://en.wikipedia.org/wiki/Software_testing)

[2] "Jest," jestjs.io. [Online]. Available: [https://jestjs.io](https://jestjs.io)

[3] "Vitest," vitest.dev. [Online]. Available: [https://vitest.dev/](https://vitest.dev/)

[4] OpenJS Foundation, "Test runner," Node.js API Docs. [Online]. Available: [https://nodejs.org/api/test.html](https://nodejs.org/api/test.html)

[5] "Cypress," cypress.io. [Online]. Available: [https://www.cypress.io/](https://www.cypress.io/)

[6] "Playwright," playwright.dev. [Online]. Available: [https://playwright.dev/](https://playwright.dev/)

```quiz
Q: On the testing spectrum, a unit test is characterized by…
- driving a real browser through the whole app
- testing one function in isolation, fast, with dependencies mocked
correct: 1
explain: Unit tests focus on a single unit (a function) with its dependencies replaced by mocks. They are fast and pinpoint failures, but only prove the unit works in isolation.

Q: What makes Vitest attractive over Jest for new Vite-based projects?
- It has a visual test runner like Cypress
- It is Vite-native, with first-class ESM and TypeScript support and faster startup, while keeping a Jest-compatible API
correct: 1
explain: Vitest runs on Vite, so ESM, TypeScript, and JSX work out of the box without Babel config. Its API mirrors Jest, so tests port easily, and startup is faster because it reuses Vite's pipeline.

Q: When would you reach for node:test over Vitest or Jest?
- When you want a visual browser-based runner
- When you want zero test-tooling dependencies — it ships with Node itself
correct: 1
explain: node:test is Node's built-in runner. For a library or CLI that values a minimal install and no dev dependencies for testing, it provides the describe/it/assert shape with nothing extra to install.

Q: The key difference between Playwright and a unit test framework is…
- Playwright is faster
- Playwright drives a real browser to simulate user behavior, testing the whole rendered app; unit frameworks test functions in isolation
correct: 1
explain: Playwright automates Chromium, Firefox, and WebKit to click, type, and assert against rendered pages — end-to-end testing. Unit frameworks never spin up a browser; they call functions directly.

Q: Why does the "testing pyramid" recommend more unit tests than E2E tests?
- Unit tests are more important than E2E tests
- Unit tests are fast and pinpoint failures, so many are affordable; E2E tests are slow and costly, so a few critical flows suffice
correct: 1
explain: The pyramid optimizes for fast feedback. Many unit tests catch the bulk of regressions in seconds; a smaller set of E2E tests covers critical user flows. Inverting it (many E2E, few unit) makes the suite slow and brittle.
```
