September 28, 2026

Testing JavaScript & TypeScript dengan Vitest.

Setup Vitest untuk proyek JavaScript/TypeScript, menulis unit test dengan describe/it/expect, mocking dengan vi.fn, vi.spyOn, vi.mock, dan fake timers, test async, coverage, serta component test React dengan Testing Library.

Vitest adalah test runner yang dibangun di atas Vite. Karena memakai pipeline transformasi yang sama dengan aplikasi, TypeScript, JSX, alias @/, dan plugin Vite langsung bekerja tanpa konfigurasi Babel/ts-jest. API-nya kompatibel dengan Jest (describe, it, expect), jadi migrasi biasanya mulus. Konsep umumnya (AAA, mocking, piramida) ada di Dasar Testing .

Catatan versi: saat catatan ini ditulis, dokumentasi resmi menampilkan Vitest v5, yang membutuhkan Vite ≥ 6.4 dan Node.js ≥ 22.12.


1. Setup

npm install -D vitest

package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "coverage": "vitest run --coverage"
  }
}
PerintahPerilaku
vitestMode watch (default di terminal lokal); menjalankan ulang test terkait saat file berubah
vitest runSekali jalan — untuk CI
vitest run src/utilsFilter berdasarkan path
vitest -t "diskon"Filter berdasarkan nama test
vitest --uiUI di browser (butuh @vitest/ui)

Vitest membaca vite.config.ts bila ada. Untuk proyek non-Vite atau konfigurasi khusus test, buat vitest.config.ts:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",       // "jsdom" / "happy-dom" untuk kode browser
    include: ["src/**/*.{test,spec}.{ts,tsx}"],
  },
});

File test harus mengandung .test. atau .spec. di namanya, misalnya harga.test.ts.


2. Unit Test Pertama

// src/utils/slug.ts
export function slugify(teks: string): string {
  return teks
    .toLowerCase()
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}
// src/utils/slug.test.ts
import { describe, expect, it } from "vitest";
import { slugify } from "./slug";

describe("slugify", () => {
  it("mengubah spasi menjadi tanda hubung", () => {
    expect(slugify("Belajar Vitest Dasar")).toBe("belajar-vitest-dasar");
  });

  it("membuang karakter spesial & aksen", () => {
    expect(slugify("  Café & Kopi!  ")).toBe("cafe-kopi");
  });

  it.each([
    ["TypeScript 6", "typescript-6"],
    ["Node.js", "node-js"],
    ["---", ""],
  ])("slugify(%s) → %s", (input, hasil) => {
    expect(slugify(input)).toBe(hasil);
  });
});

it.each menghindari copy-paste untuk kasus tabel.

Matcher yang sering dipakai

MatcherKegunaan
toBe(x)Kesamaan persis (Object.is) — untuk primitif
toEqual(obj)Kesamaan struktur (deep)
toStrictEqual(obj)Seperti toEqual + cek undefined & tipe class
toContain(x)Array/string berisi x
toMatchObject(obj)Objek mengandung subset properti
toThrow(/pesan/)Fungsi melempar error
toBeCloseTo(0.3)Angka desimal (hindari 0.1 + 0.2 !== 0.3)
resolves / rejectsUntuk Promise

3. Test Async

// src/api/user.ts
export async function getUser(id: number) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as { id: number; nama: string };
}
import { afterEach, expect, it, vi } from "vitest";
import { getUser } from "./user";

afterEach(() => {
  vi.unstubAllGlobals();
});

it("mengembalikan user dari API", async () => {
  vi.stubGlobal(
    "fetch",
    vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ id: 1, nama: "Fanny" }), { status: 200 })
    )
  );

  await expect(getUser(1)).resolves.toEqual({ id: 1, nama: "Fanny" });
});

it("melempar error saat 404", async () => {
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status: 404 })));

  await expect(getUser(99)).rejects.toThrow("HTTP 404");
});

Selalu await (atau return) promise di test; tanpa itu test bisa selesai sebelum assertion dijalankan. Detail fetch ada di catatan Fetch API & JSON .


4. Mocking dengan vi

APIFungsi
vi.fn()Membuat fungsi palsu yang mencatat pemanggilan
vi.spyOn(obj, "method")Memata-matai method asli (bisa diganti implementasinya)
vi.mock("./modul")Mengganti seluruh modul (di-hoist ke atas file)
vi.stubGlobal("fetch", fn)Mengganti global
vi.useFakeTimers()Mengendalikan setTimeout, Date
vi.restoreAllMocks()Mengembalikan spy ke implementasi asli

vi.mock — mengganti modul

// src/services/order.ts
import { kirimEmail } from "./mailer";

export async function buatOrder(email: string, total: number) {
  const order = { id: crypto.randomUUID(), email, total };
  await kirimEmail(email, `Order ${order.id} diterima`);
  return order;
}
import { expect, it, vi } from "vitest";
import { buatOrder } from "./order";
import { kirimEmail } from "./mailer";

vi.mock("./mailer", () => ({
  kirimEmail: vi.fn().mockResolvedValue(undefined),
}));

it("mengirim email konfirmasi", async () => {
  const order = await buatOrder("[email protected]", 150000);

  expect(kirimEmail).toHaveBeenCalledWith("[email protected]", expect.stringContaining(order.id));
});

Fake timers

import { expect, it, vi } from "vitest";

function debounce(fn: () => void, ms: number) {
  let t: ReturnType<typeof setTimeout>;
  return () => { clearTimeout(t); t = setTimeout(fn, ms); };
}

it("debounce hanya memanggil sekali", () => {
  vi.useFakeTimers();
  const fn = vi.fn();
  const d = debounce(fn, 300);

  d(); d(); d();
  vi.advanceTimersByTime(300);

  expect(fn).toHaveBeenCalledTimes(1);
  vi.useRealTimers();
});

5. Component Test React dengan Testing Library

Testing Library mendorong test yang berinteraksi seperti user: mencari elemen berdasarkan role dan teks, bukan class atau struktur DOM.

npm install -D jsdom @testing-library/react @testing-library/dom \
  @testing-library/user-event @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./vitest.setup.ts"],
  },
});
// vitest.setup.ts
import "@testing-library/jest-dom/vitest"; // matcher: toBeInTheDocument, toBeDisabled, ...
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";

afterEach(() => cleanup()); // wajib jika globals: false

Komponen yang diuji:

// src/components/Counter.tsx
import { useState } from "react";

export function Counter({ max = 3 }: { max?: number }) {
  const [n, setN] = useState(0);
  return (
    <div>
      <p>Jumlah: {n}</p>
      <button onClick={() => setN((x) => x + 1)} disabled={n >= max}>
        Tambah
      </button>
    </div>
  );
}
// src/components/Counter.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, it } from "vitest";
import { Counter } from "./Counter";

it("menambah angka dan berhenti di batas maksimum", async () => {
  const user = userEvent.setup();
  render(<Counter max={2} />);

  const tombol = screen.getByRole("button", { name: "Tambah" });
  await user.click(tombol);
  await user.click(tombol);

  expect(screen.getByText("Jumlah: 2")).toBeInTheDocument();
  expect(tombol).toBeDisabled();
});

Urutan prioritas query: getByRole → getByLabelText → getByText → getByTestId (terakhir). Query berbasis role sekaligus menguji aksesibilitas. Dasar komponen dan state ada di Cara Menggunakan React .

Varian queryJika tidak ketemuUntuk
getBy…ThrowElemen yang harus ada
queryBy…nullMemastikan elemen tidak ada
findBy…Promise, menungguElemen yang muncul async

6. Coverage

npm install -D @vitest/coverage-v8
npx vitest run --coverage

Laporan HTML ada di coverage/index.html. Jadikan coverage alat untuk menemukan bagian yang belum diuji, bukan target angka.


Kesalahan Umum

  1. Lupa await pada user.click, findBy…, atau expect(...).resolves → test lulus palsu atau warning act(...).
  2. Environment salah. Test komponen di environment: "node" gagal dengan document is not defined.
  3. Mock bocor antar test. Tambahkan vi.restoreAllMocks() di afterEach, atau set restoreMocks: true di config.
  4. vi.mock dengan path berbeda dari yang di-import modul yang diuji → mock tidak terpakai.
  5. Query berdasarkan class/struktur DOM (container.querySelector(".btn")) → test patah saat styling berubah.
  6. Menguji state internal (useState) alih-alih apa yang terlihat user.
  7. Menjalankan vitest (watch) di CI. Gunakan vitest run agar proses selesai.

Ringkasan

  • npm i -D vitest, file *.test.ts, vitest untuk watch dan vitest run untuk CI.
  • vi.fn, vi.spyOn, vi.mock, vi.stubGlobal, dan fake timers untuk mengisolasi dependensi.
  • React: jsdom + Testing Library + user-event, query berdasarkan role.
  • Untuk alur di browser sungguhan, lanjut ke E2E dengan Playwright .

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments