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"
}
}
| Perintah | Perilaku |
|---|---|
vitest | Mode watch (default di terminal lokal); menjalankan ulang test terkait saat file berubah |
vitest run | Sekali jalan — untuk CI |
vitest run src/utils | Filter berdasarkan path |
vitest -t "diskon" | Filter berdasarkan nama test |
vitest --ui | UI 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
| Matcher | Kegunaan |
|---|---|
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 / rejects | Untuk 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
| API | Fungsi |
|---|---|
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 query | Jika tidak ketemu | Untuk |
|---|---|---|
getBy… | Throw | Elemen yang harus ada |
queryBy… | null | Memastikan elemen tidak ada |
findBy… | Promise, menunggu | Elemen 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
- Lupa
awaitpadauser.click,findBy…, atauexpect(...).resolves→ test lulus palsu atau warningact(...). - Environment salah. Test komponen di
environment: "node"gagal dengandocument is not defined. - Mock bocor antar test. Tambahkan
vi.restoreAllMocks()diafterEach, atau setrestoreMocks: truedi config. vi.mockdengan path berbeda dari yang di-import modul yang diuji → mock tidak terpakai.- Query berdasarkan class/struktur DOM (
container.querySelector(".btn")) → test patah saat styling berubah. - Menguji state internal (
useState) alih-alih apa yang terlihat user. - Menjalankan
vitest(watch) di CI. Gunakanvitest runagar proses selesai.
Ringkasan
npm i -D vitest, file*.test.ts,vitestuntuk watch danvitest rununtuk 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.