After this lesson, you will be able to: Write unit, integration, and end-to-end tests for JavaScript/TypeScript (Vitest or Jest) and Python (pytest), understand test-driven development, and decide what to test.
Testing is how you ship without fear. This lesson covers the three test layers, what each is for, how to set up Vitest in a JavaScript project and pytest in a Python one, and what test-driven development actually is.
Unit tests: test ONE function in isolation. Fast (milliseconds), many (hundreds). Use them to lock in pure logic. Integration tests: test how multiple modules wire together (e.g. your API route calling the DB). Slower (seconds), fewer (dozens). Use them to catch boundary bugs. End-to-end (e2e) tests: drive the real app from outside (a browser, an HTTP client). Slowest (tens of seconds), fewest (handful). Use them to verify the critical user journeys work.
Default: lots of unit tests, a moderate number of integration tests, a handful of e2e tests. When the codebase has good types and few pure functions (e.g. a thin web app), inverting the pyramid (heavy integration, light unit) can give more confidence per test. What matters is: tests catch real regressions and run fast enough that you actually run them.
Vitest is the modern Vite-native test runner. It works for any TS/JS project even without Vite.
// package.json{"scripts": { "test": "vitest" },"devDependencies": { "vitest": "^1.0.0" }}// src/math.tsexport function add(a: number, b: number): number {return a + b;}// src/math.test.tsimport { describe, it, expect } from "vitest";import { add } from "./math";describe("add", () => {it("sums two positive numbers", () => {expect(add(2, 3)).toBe(5);});it("handles negatives", () => {expect(add(-1, 1)).toBe(0);});it("handles zero", () => {expect(add(0, 0)).toBe(0);});});// Run: npm test// Watch mode: npm test -- --watch
Use a test database (Docker Postgres or a local SQLite for speed). Never test against production.
// src/orders.test.tsimport { beforeEach, afterAll, describe, it, expect } from "vitest";import { db } from "./db";import { createOrder, listOrdersForUser } from "./orders";beforeEach(async () => {await db.query("TRUNCATE orders RESTART IDENTITY CASCADE");});afterAll(async () => { await db.end(); });describe("orders", () => {it("persists a new order", async () => {const order = await createOrder({ userId: "u1", total: 42.00 });const list = await listOrdersForUser("u1");expect(list).toHaveLength(1);expect(list[0].total).toBe(42.00);});});
pytest is the standard Python test runner. Tests are plain functions named test_*; assertions use the built-in assert.
# math.pydef add(a, b):return a + b# test_math.py (pytest discovers test_*.py files and test_* functions)from math import adddef test_sums_two_positives():assert add(2, 3) == 5def test_handles_negatives():assert add(-1, 1) == 0# Fixtures provide reusable setup (like a temp DB connection):import pytest@pytest.fixturedef sample_user():return {"id": "u1", "name": "Alex"}def test_uses_fixture(sample_user):assert sample_user["name"] == "Alex"# Install: pip install pytest Run: pytest Coverage: pytest --cov
TDD flips the order: write a failing test FIRST (red), write the minimum code to make it pass (green), then clean up the code with the test as your safety net (refactor). Repeat in small loops. When it shines: the requirements are clear but the design is fuzzy. Writing the test first forces you to design the interface from the caller's point of view before you commit to an implementation. When to skip it: exploratory or throwaway code, or when the design is already obvious (write the test right after). TDD is a tool, not a religion. The goal is working, tested code, whichever order gets you there.
Test: business rules, edge cases, regression scenarios you've actually hit, anything that's broken before. Test: pure functions; the result depends only on the input, so the test is short and reliable. Don't test: framework code (React's useState works; you don't need to verify it). Don't test: trivial getters or types (the type checker handles them). Don't test: implementation details that will change as you refactor. Test BEHAVIOR (what the user sees), not internals.
Mocking everything until tests pass but the real system is broken. Tests should exercise real code where possible. Skipping the regression test after a bug fix. The next deploy can silently undo your work. Brittle tests that break on every refactor. Usually means they test implementation, not behavior. Tests that depend on time, network, or random values without being controlled. Use fake timers, mock APIs, fixed seeds. Treating coverage % as the goal. 90% coverage of trivial code teaches you less than 30% coverage of the critical paths.
Pick the most likely diagnosis.
Sign in and purchase access to unlock this lesson.