Testing — Vitest, Jest & Playwright

Test ছাড়া কোড মানে — ভবিষ্যতের bug

~35 min Intermediate 8 practice problems Live runner

1. Why Test

  • Catch regressions when you refactor
  • Document expected behaviour as runnable specs
  • Force you to write smaller, more decoupled functions
  • Catch the "I forgot the edge case" failures before users do
Test লেখা মানে আপনি নিজেই নিজের কোডের প্রথম user। ভবিষ্যতে refactor করার সাহস পাবেন এবং যেকোনো breaking change তৎক্ষণাৎ ধরা পড়বে।

2. The Test Pyramid

LayerToolSpeedCoverage
Unit (single function)Vitest, JestMilliseconds~70% of tests
Integration (module + module)VitestTens of ms~20%
E2E (real browser)Playwright, CypressSeconds~10%

3. Vitest in 30 Seconds

$ npm install -D vitest

// math.js
export const add = (a, b) => a + b;

// math.test.js
import { describe, it, expect } from "vitest";
import { add } from "./math.js";

describe("add", () => {
    it("sums positive numbers",  () => expect(add(2, 3)).toBe(5));
    it("works with negatives",   () => expect(add(-1, 1)).toBe(0));
    it("throws nothing weird",   () => expect(add(0.1, 0.2)).toBeCloseTo(0.3));
});

// package.json
"scripts": { "test": "vitest" }

$ npm test          # watch mode by default

4. Common Matchers

expect(x).toBe(y);             // ===
expect(arr).toEqual(other);    // deep equal
expect(arr).toContain(item);
expect(fn).toThrow();
expect(fn).toThrow(/network/);
expect(s).toMatch(/regex/);
expect(p).resolves.toBe(42);   // for promises
expect(p).rejects.toThrow();

5. Async Tests

it("fetches data", async () => {
    const data = await getUser(1);
    expect(data.id).toBe(1);
});

6. Sandbox-Safe "Mini Test Runner"

To get a feel for what the matchers do, here's a 12-line runner you can run in this page.

tiny-test.js
function expect(actual) {
    return {
        toBe(exp) {
            if (actual !== exp) throw new Error(`${actual} !== ${exp}`);
        },
        toEqual(exp) {
            if (JSON.stringify(actual) !== JSON.stringify(exp))
                throw new Error("deep mismatch");
        },
        toThrow() {
            try { actual(); }
            catch { return; }
            throw new Error("didn't throw");
        }
    };
}

function test(name, fn) {
    try { fn(); console.log("✓", name); }
    catch (e) { console.log("✗", name, "-", e.message); }
}

const add = (a, b) => a + b;

test("add 2+3", () => expect(add(2, 3)).toBe(5));
test("add -1+1", () => expect(add(-1, 1)).toBe(0));
test("intentional fail", () => expect(add(2, 2)).toBe(5));
test("deep equal", () =>
    expect([1, 2]).toEqual([1, 2]));

7. Mocking

import { vi } from "vitest";

it("calls onSave with right value", () => {
    const onSave = vi.fn();
    submitForm({ name: "Arif" }, onSave);
    expect(onSave).toHaveBeenCalledWith({ name: "Arif" });
});

// Mock a module
vi.mock("./db.js", () => ({ load: () => ({ id: 1 }) }));

8. End-to-End with Playwright

$ npm init playwright@latest

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

test("user can sign up", async ({ page }) => {
    await page.goto("http://localhost:5173");
    await page.getByLabel("Email").fill("a@x.com");
    await page.getByLabel("Password").fill("hunter2!");
    await page.getByRole("button", { name: "Sign up" }).click();
    await expect(page.getByText("Welcome")).toBeVisible();
});

9. Glossary (শব্দকোষ)

TermMeaningবাংলায়
Unit testTests one function in isolation; very fast, deterministic.এক function-এর isolated test; দ্রুত ও deterministic।
Integration testTests two or more modules together.একাধিক module একসাথে test।
E2E testDrives the real browser through the app like a user.Real browser-এ user-এর মতো test।
VitestModern, fast, Vite-native unit test runner.Vite-based আধুনিক fast unit test runner।
JestPopular older unit test runner.জনপ্রিয় পুরোনো test runner।
PlaywrightCross-browser E2E test framework.Cross-browser E2E framework।
MatchertoBe, toEqual, toThrow — assertion verbs.Assertion-এর verb — toBe ইত্যাদি।
MockFake replacement for a dependency; lets you isolate logic.Dependency-র fake replacement।
Coverage% of lines/branches exercised by tests.Test দ্বারা ছোঁয়া code-এর শতাংশ।
Test pyramidMany unit, some integration, few E2E.অনেক unit, কিছু integration, অল্প E2E।
মনে রাখুন: Test লেখা মানে নিজের কোডের প্রথম user হওয়া; refactor-এ আত্মবিশ্বাস আসে। Pyramid মেনে চলুন — সব E2E করলে slow ও flaky, সব unit-ও integration bug ধরবে না। ১০০% coverage-এর পেছনে দৌড়াবেন না; ৭০-৮৫%-এ logic-heavy code-এ deep test অনেক বেশি কার্যকর।

10. Practice Problems

  1. Write 3 tests for a divide(a, b) function (positive, negative, b=0).
    ✨ Show Answer
    a1.js
    const divide = (a, b) => { if (b === 0) throw new Error("div0"); return a / b; };
    function test(name, fn) {
        try { fn(); console.log("✓", name); }
        catch (e) { console.log("✗", name, e.message); }
    }
    test("10/2", () => { if (divide(10, 2) !== 5) throw new Error("x"); });
    test("-6/3", () => { if (divide(-6, 3) !== -2) throw new Error("x"); });
    test("div0 throws", () => {
        try { divide(1, 0); throw new Error("didn't throw"); }
        catch (e) { if (e.message !== "div0") throw e; }
    });
  2. Test debounce with a fake timer (logic).
    ✨ Show Answer
    // Vitest fake timers
    import { vi } from "vitest";
    
    it("debounce fires only after silence", () => {
        vi.useFakeTimers();
        const fn = vi.fn();
        const d = debounce(fn, 100);
        d(); d(); d();
        vi.advanceTimersByTime(100);
        expect(fn).toHaveBeenCalledTimes(1);
    });
  3. Why does aiming for 100% test coverage often backfire?
    ✨ Show Answer

    Answer: The last 5–10% comes from trivial getters and one-line passthroughs that catch nothing. You spend hours mocking the world to lift the number — and the resulting tests are brittle, slow, and produce false confidence. Aim for 70–85% with sharp tests on the logic that actually matters.

  4. Build a runnable assertion that an array is sorted.
    ✨ Show Answer
    a4.js
    const isSorted = a => a.every((v, i) => i === 0 || a[i - 1] <= v);
    console.log(isSorted([1, 2, 2, 3]));
    console.log(isSorted([2, 1]));
  5. Compare unit test vs E2E in two sentences.
    ✨ Show Answer

    Answer: Unit tests check a single function in isolation — fast, deterministic, useful for refactor confidence. E2E tests drive a real browser through the actual UI — slow, more flaky, but they catch the integration bugs that unit tests cannot.

  6. Sketch a Playwright test for a login flow.
    ✨ Show Answer
    test("login", async ({ page }) => {
        await page.goto("/login");
        await page.getByLabel("Email").fill("a@x.com");
        await page.getByLabel("Password").fill("pw");
        await page.getByRole("button", { name: "Sign in" }).click();
        await expect(page.getByText("Dashboard")).toBeVisible();
    });
  7. Why is mocking the network usually better than hitting it in tests?
    ✨ Show Answer

    Answer: Real network is slow, flaky, and depends on services you don't control. A mock gives instant deterministic responses — the test runs in milliseconds and never fails because someone else's API is down. Reserve real-network tests for one or two end-to-end "smoke" runs.

  8. In one paragraph, when should you write a test before the code?
    ✨ Show Answer

    Answer: Test-first works best for pure functions with clear inputs/outputs (parsers, validators, math), where writing the test forces you to nail the contract. For UI work or research code, writing tests after a working sketch is usually faster — premature tests against unstable APIs slow you down. Pragmatism beats dogma.

Summary — Module 37

Test pyramid: many unit tests, some integration, a few E2E. Vitest is the modern fast unit runner; Playwright owns end-to-end. Mock the network in unit tests; hit it in E2E. Aim for high-leverage coverage, not 100%.

Test pyramid মেনে চলুন — অনেক unit, কিছু integration, কয়েকটি E2E। Vitest unit-এ দ্রুত; Playwright E2E-তে real browser চালায়।

Next Module → Node.js, files, HTTP, Express।