Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 12 min read

QA and Test Automation: Unit, Integration, and E2E Explained

A practical guide to the testing pyramid — what unit, integration, and end-to-end tests actually do, when to write each, and how to automate them in CI without slowing your team down.

By codefunc

qatestingunit-testsintegration-testse2eautomationvitestci

Manual QA does not scale. Neither does a test suite that takes forty minutes and flakes every Tuesday. Effective quality assurance in 2026 is layered automation: fast tests that run on every save, broader tests that run on every pull request, and a thin slice of end-to-end tests that prove critical user journeys still work in a real browser.

Bottom line: Write many unit tests for pure logic, some integration tests for modules working together, and few E2E tests for paths that earn revenue or prevent disasters. Automate all three in CI — but keep the pyramid shape or feedback gets slow and teams start skipping runs.

What QA automation actually means

Quality assurance asks: does the product meet requirements and work for users?
Test automation asks: can a machine verify that repeatedly, cheaply, and on every change?

Automation does not replace exploratory testing, design review, or accessibility audits. It prevents regressions — the bug you fixed in March that returned in October because nobody re-checked manually.

Activity Human Automated
Exploratory testing
Regression on every PR impractical
Edge case matrix (100 inputs) tedious
"Does this feel right?" UX
Build/deploy integrity

The testing pyramid

Mike Cohn's testing pyramid still holds: many small fast tests at the base, fewer slow tests at the top.

flowchart TB
  subgraph pyramid [Test pyramid]
    E2E["E2E — few, slow, high confidence"]
    INT["Integration — some, medium speed"]
    UNIT["Unit — many, fast, cheap"]
  end
  UNIT --> INT --> E2E
Layer Speed Cost Confidence scope
Unit milliseconds low One function or class
Integration seconds medium Several modules + I/O boundaries
E2E minutes high Full app in browser

Anti-pyramid (inverted cone) — mostly E2E, few unit tests — is the main reason teams abandon CI. Fix the base first.

Unit tests: prove the logic

Unit tests exercise a single unit of code in isolation — usually one function or method — with dependencies mocked or stubbed.

What they catch

  • Wrong output for known input
  • Missing edge cases (empty string, null, malformed JSON)
  • Regressions when refactoring internals
  • Error messages and validation rules

What they do not catch

  • Two modules wired together incorrectly
  • CSS breaking layout
  • API contract drift between frontend and backend
  • Browser-specific behavior

Characteristics of good unit tests

Property Meaning
Fast Entire suite under a few seconds
Deterministic Same result every run — no network roulette
Isolated No real database, filesystem, or HTTP
Readable Name describes behavior: rejects empty format input

Example: pure tool logic

Developer tool sites benefit enormously from unit tests because core value lives in pure functions — formatters, parsers, encoders — not in exotic infrastructure.

import { describe, it, expect } from "vitest";
import { formatJson } from "../json";
import { expectErr, expectOk } from "./helpers";

describe("formatJson", () => {
  it("formats json with indentation", () => {
    expectOk(formatJson('{"a":1}'), "\n");
  });

  it("rejects empty input", () => {
    expectErr(formatJson(""), "Paste JSON");
  });
});

On codefunc, logic lives in lib/tools/logic/ and runs with Vitest + happy-dom when DOM APIs are needed. npm test runs the full unit suite in seconds — suitable for pre-commit and every PR.

What to unit test first (ROI order)

  1. Parsers and validators — JSON, JWT payload decode, regex, cron, robots.txt
  2. Transformers — encode/decode, format/minify, diff algorithms
  3. SEO/metadata helpers — title truncation, canonical URL building
  4. Registry invariants — unique slugs, every tool has a loader entry

Skip unit testing presentational components with no logic on day one. Test the behavior users depend on.

Unit test anti-patterns

Anti-pattern Problem
Testing implementation details Refactors break tests without user-visible bugs
Mocking everything Test proves mocks work, not production code
One assertion per file obsession Verbose without clarity
Snapshot of 4,000 lines of HTML Unreviewable diffs; mute failures
No tests for error paths Happy path only — prod fails on empty input

Use the JSON Validator and Regex Tester locally to explore edge cases before encoding them as test fixtures.

Integration tests: prove the wiring

Integration tests verify that multiple parts work together — modules, database layer, file system, HTTP client against a test server, or React component with real child providers.

What they catch

  • Schema mismatch between layers
  • Wrong export/import after refactor
  • ORM queries that unit mocks hid
  • Component + context + hook integration bugs
  • Build-time static generation failures (in SSG apps)

Boundaries to test

Boundary Example
Logic + file I/O Blog post parser reading markdown frontmatter
API route + validation POST handler rejects malformed body
Component + state Search dialog opens and filters tools list
Package + dependency Cron parser library with your wrapper
Build pipeline next build generates all static pages

Integration vs unit — decision rule

If removing a mock would make the test fail because real collaboration is under test — it is integration.
If every dependency is fake and only one function's return value matters — it is unit.

Example: markdown blog pipeline

An integration test for a static blog might read a real fixture file and assert parsed metadata:

import { describe, it, expect } from "vitest";
import { getPostBySlug } from "../posts";

describe("blog posts", () => {
  it("parses frontmatter and computes reading time", () => {
    const post = getPostBySlug("how-to-format-json-safely");
    expect(post?.title).toBeTruthy();
    expect(post?.readingTimeMinutes).toBeGreaterThan(0);
    expect(post?.category).toBe("guides");
  });
});

This tests gray-matter + file read + slug resolution together — not any single function in isolation.

Example: production build as integration gate

For Next.js SSG sites, npm run build is one of the highest-value integration tests:

  • Every generateStaticParams slug resolves
  • No broken imports in tool loaders
  • OG image routes compile
  • Sitemap and robots generate

If build fails in CI, you caught a class of bugs no unit test touched — missing registry entry, typo in slug, edge runtime on static export.

Integration test anti-patterns

Anti-pattern Problem
Shared mutable DB state Order-dependent flakes
Hitting production APIs Rate limits, data corruption, flakes
Testing entire app without boundaries Slow; duplicates E2E
No cleanup between tests Polluted state

Use test containers, in-memory SQLite, or fixture files. Reset state per test.

E2E tests: prove the user journey

End-to-end (E2E) tests drive the application like a user — typically in a real browser via Playwright or Cypress. They click, type, navigate, and assert on visible outcomes.

What they catch

  • Broken navigation and routing
  • Form submission flows across pages
  • Auth and session regressions
  • JavaScript errors that only appear with full bundle
  • Visual/layout breaks on critical screens (with screenshot diff optional)

What they cost

Cost Detail
Time 30s–5min per spec
Flakiness Timing, animations, network, test data
Maintenance UI selectors break when design changes
CI resources Browser binaries, parallelization complexity

That is why the pyramid says few E2E tests — only for journeys that justify the price.

What to cover with E2E (smoke list)

Pick 5–10 flows, not 500:

Priority Flow
P0 Homepage loads, core navigation visible
P0 Primary conversion path (signup, checkout, tool use)
P1 Search or command palette finds a result
P1 One representative tool: input → output → copy
P2 Blog post renders, internal links work
P2 Legal pages, 404 page

For a developer tools catalog, a strong minimal E2E suite:

  1. Open /tools/json-formatter
  2. Paste sample JSON, verify formatted output appears
  3. Open command search, navigate to JWT Decoder
  4. Confirm homepage lists categories

You do not need E2E for all 72 tools if unit tests cover each formatter's logic.

Example: Playwright smoke spec

import { test, expect } from "@playwright/test";

test("json formatter transforms input", async ({ page }) => {
  await page.goto("/tools/json-formatter");
  await page.getByRole("textbox", { name: /input/i }).fill('{"a":1}');
  await expect(page.getByRole("textbox", { name: /output/i })).toContainText('"a"');
});

test("command search opens with keyboard", async ({ page }) => {
  await page.goto("/");
  await page.keyboard.press("Control+K");
  await expect(page.getByRole("dialog")).toBeVisible();
});

Prefer accessible selectors (getByRole, getByLabel) over CSS classes — they survive refactors and align with accessibility best practices.

E2E anti-patterns

Anti-pattern Problem
200 E2E tests, 10 unit tests 45-minute CI, random failures
sleep(3000) everywhere Flaky; use auto-wait assertions
Testing third-party widgets You do not control Google Analytics
Same flow in 4 browsers on every commit Save cross-browser for nightly runs
Hard-coded prod URLs Never mutate production from tests

Run E2E against preview deploys or local npm run start after build — not live production.

Comparing the three layers

Question Unit Integration E2E
Scope One function Several modules Whole application
Dependencies Mocked Real (or test doubles at boundary) Real stack
Speed ⚡ ms 🔄 seconds 🐢 minutes
Debug ease Pinpoints line Narrow subsystem "Something broke"
Best for Algorithms, validation APIs, build, wiring Critical user paths
Typical tools Vitest, Jest Vitest, Supertest Playwright, Cypress

Where manual QA still fits

Automate regression; keep humans for:

  • Exploratory testing — "what if I paste a 10MB file?"
  • Usability — is the flow intuitive?
  • Accessibility — keyboard + screen reader passes
  • Visual design — spacing, motion, brand feel
  • New features — before automation exists

Exploratory findings become new automated tests so bugs stay dead.

CI pipeline: wiring all three layers

flowchart LR
  A[Push / PR] --> B[Lint]
  B --> C[Unit tests]
  C --> D[Integration tests]
  D --> E[Build]
  E --> F{Main branch?}
  F -->|PR only| G[Done]
  F -->|Main / nightly| H[E2E smoke]
  H --> I[Deploy]
Stage Runs on Fail merge?
Lint + typecheck Every PR Yes
Unit tests Every PR Yes
Integration (+ build) Every PR Yes
E2E smoke Main, nightly, or pre-release Yes for release
Full E2E cross-browser Nightly Alert, not block

Standard release command for many frontend repos:

npm run lint && npm test && npm run build

Add playwright test when E2E exists — not before the base is green.

Parallelization and caching

  • Shard unit tests by file across CI workers
  • Cache node_modules and Playwright browsers
  • Run E2E only on changed paths when monorepo tooling supports it

Test data and fixtures

Layer Data strategy
Unit Inline strings, smallest possible input
Integration Fixture files in __fixtures__/
E2E Seed script or API factory; reset DB per run

Generate realistic JSON payloads for fixtures with the Fake Data Generator. Compare expected vs actual output with the Text Diff tool when debugging failing assertions.

Store golden files for complex output only when humans review changes intentionally — not for volatile timestamps or IDs.

Coverage: useful metric, dangerous target

npm run test:coverage helps find untested modules — not prove quality.

Coverage % Reality
0% on logic folder Dangerous for tool sites
80% on lib/tools/logic/ Strong for formatters
100% line coverage mandate Encourages useless tests

Cover branches that handle errors and public APIs. Ignore generated files, config, and thin re-exports.

Testing strategy by project type

Static developer tools (like codefunc)

Layer Focus
Unit All lib/tools/logic/
Integration Blog parser, metadata, build
E2E 3–5 smoke journeys
Manual New tool UX, a11y

SaaS with auth and billing

Layer Focus
Unit Business rules, pricing, permissions
Integration API routes, DB, webhooks
E2E Login, checkout, core feature
Manual Edge cases, email delivery

Design system / component library

Layer Focus
Unit Utility functions
Integration Components + providers (Vitest + Testing Library)
E2E Visual regression (Storybook, Chromatic) or key docs pages

30-day QA automation rollout

Week 1 — Unit foundation

  • Pick test runner (Vitest/Jest) and npm test script
  • Test top 5 most-used pure functions
  • Add CI job that fails on red tests

Week 2 — Integration boundaries

  • Add build step to CI (npm run build)
  • One integration test per critical pipeline (API, parser, auth)
  • Document how to run full check locally

Week 3 — E2E smoke

  • Install Playwright or Cypress
  • Write 3 smoke specs for P0 flows
  • Run E2E on main branch only

Week 4 — Harden

  • Fix or delete flaky tests — zero tolerance
  • Add coverage report for logic folder
  • Turn last production bug into a regression test

Anti-patterns that kill QA programs

Symptom Fix
"CI is red, merge anyway" Block merges; fix trust
Tests nobody reads when they fail Delete or fix — noise kills culture
QA team only, devs never write tests Quality is everyone's job
Testing after release crunch Shift left — test with feature
E2E only because "it's real" Add unit tests for debuggability

Practical takeaway

Unit tests protect logic. Integration tests protect connections. E2E tests protect journeys. Automate all three, but respect the pyramid — fast feedback on every commit, deep confidence before release.

Start where bugs hurt most: validators and parsers for tool sites, auth and payments for SaaS. Let build be your integration gate. Add browser smoke tests when the cost of a broken homepage exceeds an hour of Playwright maintenance.

Good QA is not more manual checklists. It is machines repeating what you already learned — so you never learn the same bug twice.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool