Panduan Vitest Lanjutan dengan Claude Code
Rancang Vitest dengan Claude Code: tiruan, timer palsu, jsdom, cakupan, cuplikan, dan CI.
Masalah yang Diselesaikan Alur Vitest Ini
Meminta Claude Code “tambahkan pengujian Vitest” saja tidak cukup. Pengujian bisa lulus di lokal, tetapi gagal saat menyentuh waktu, DOM, API eksternal, atau CI. Panduan ini merapikan area berisiko menjadi satu alur praktis: tiruan untuk mengganti dependensi, timer palsu untuk mengendalikan jam, cakupan untuk melihat cabang yang belum diuji, jsdom untuk struktur DOM, cuplikan kecil untuk kontrak tampilan, dan perintah CI yang benar-benar selesai.
Pada 22 Juli 2026, artikel ini dicocokkan dengan panduan CLI Vitest, konfigurasi watch, dan panduan migrasi Vitest 4. Vitest 4 memerlukan Vite 6 atau lebih baru dan Node 20 atau lebih baru. Di CI atau terminal noninteraktif, vitest otomatis berjalan sekali; vitest run tetap membuat maksud script lebih jelas.
Gunakan Claude Code sebagai pasangan perancang pengujian. Jelaskan batas yang ditirukan, apakah jam harus dibekukan, apakah jsdom cukup, dan perintah apa yang membuktikan hasilnya. Untuk alur terkait, baca strategi pengujian Claude Code, panduan tiruan API dengan MSW, dan pengujian E2E Playwright.
flowchart TD
A["Spesifikasi: sukses dan gagal"] --> B["Vitest config: node/jsdom/coverage"]
B --> C["Unit: logika murni dan batas API"]
B --> D["Waktu: timer palsu dan Date tetap"]
B --> E["DOM: jsdom dan cuplikan"]
C --> F["CI: vitest run --coverage"]
D --> F
E --> F
Mulai dari Konfigurasi Stabil
Pasang Vitest, penyedia cakupan V8, jsdom, dan TypeScript. Aplikasi Vite bisa berbagi konfigurasi, tetapi vitest.config.ts khusus membuat niat pengujian mudah dibaca oleh Claude Code dan pemeriksa kode.
npm install -D vitest @vitest/coverage-v8 jsdom typescript
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: false,
restoreMocks: true,
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.d.ts", "src/**/*.test.{ts,tsx}", "src/test/**"],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
},
});
globals: false membuat asal describe dan expect terlihat jelas. restoreMocks: true mengurangi kebocoran tiruan, tetapi timer palsu dan DOM tetap harus dibersihkan sendiri.
Kasus 1: Menirukan Batas API
Pengujian unit tidak seharusnya memanggil API pesanan, pembayaran, atau pengguna yang nyata. Uji kontrak yang kamu miliki: jalur, isi permintaan, validasi input, dan perubahan galat.
// src/orders.ts
export type ApiClient = {
post<T>(path: string, body: unknown): Promise<T>;
};
export class OrderError extends Error {
constructor(message = "Order request failed") {
super(message);
this.name = "OrderError";
}
}
type OrderInput = {
sku: string;
quantity: number;
};
type OrderResponse = {
id: string;
status: "accepted" | "queued";
};
export async function createOrder(api: ApiClient, input: OrderInput) {
if (input.quantity < 1) {
throw new OrderError("Quantity must be at least 1");
}
try {
return await api.post<OrderResponse>("/orders", input);
} catch {
throw new OrderError("Order API failed");
}
}
// src/orders.test.ts
import { describe, expect, it, vi } from "vitest";
import { createOrder, type ApiClient, OrderError } from "./orders";
describe("createOrder", () => {
it("posts the order payload to the API", async () => {
const api: ApiClient = {
post: vi.fn().mockResolvedValue({ id: "ord_1", status: "accepted" }),
};
await expect(createOrder(api, { sku: "book-1", quantity: 2 })).resolves.toEqual({
id: "ord_1",
status: "accepted",
});
expect(api.post).toHaveBeenCalledWith("/orders", { sku: "book-1", quantity: 2 });
});
it("rejects invalid quantity before calling the API", async () => {
const api: ApiClient = { post: vi.fn() };
await expect(createOrder(api, { sku: "book-1", quantity: 0 })).rejects.toBeInstanceOf(
OrderError,
);
expect(api.post).not.toHaveBeenCalled();
});
it("wraps transport errors in a domain error", async () => {
const api: ApiClient = {
post: vi.fn().mockRejectedValue(new Error("ECONNRESET")),
};
await expect(createOrder(api, { sku: "book-1", quantity: 1 })).rejects.toThrow(
"Order API failed",
);
});
});
Gaya injeksi dependensi ini sering lebih mudah dibaca daripada menukar seluruh modul. vi.mock() tetap berguna, tetapi Vitest mengangkatnya sebelum import; urutan awal yang salah dapat membingungkan pemula dan hasil buatan AI.
Kasus 2: Membekukan Waktu dengan Timer Palsu
Masa uji coba, percobaan ulang, notifikasi, dan debounce menjadi rapuh jika menunggu waktu nyata. Vitest dapat mengendalikan setTimeout, setInterval, dan tanggal sistem.
// src/trial.ts
const DAY_MS = 24 * 60 * 60 * 1000;
export function getTrialEndsAt(days = 7) {
return new Date(Date.now() + days * DAY_MS).toISOString();
}
export function scheduleTrialReminder(send: () => void, days = 7) {
return setTimeout(send, days * DAY_MS);
}
// src/trial.test.ts
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getTrialEndsAt, scheduleTrialReminder } from "./trial";
describe("trial reminder", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-03T00:00:00.000Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("calculates the trial end date from the fixed clock", () => {
expect(getTrialEndsAt()).toBe("2026-06-10T00:00:00.000Z");
});
it("runs the reminder after the configured number of days", () => {
const send = vi.fn();
const timer = scheduleTrialReminder(send, 3);
vi.advanceTimersByTime(3 * 24 * 60 * 60 * 1000 - 1);
expect(send).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(send).toHaveBeenCalledTimes(1);
clearTimeout(timer);
});
});
Kesalahan paling umum adalah lupa vi.useRealTimers(). Jam palsu dari satu berkas bisa membuat berkas lain gagal sesekali. Jika ada Promise, gunakan await. Batas tanggal dan zona waktu dibahas di panduan tanggal dan waktu Claude Code.
Kasus 3: Menjaga DOM dengan jsdom dan Cuplikan
jsdom meniru API DOM di dalam Node. Ini cocok untuk struktur, teks, dan atribut aksesibilitas. Ini bukan pengganti peramban nyata untuk layout, fokus, Canvas, atau regresi visual.
// src/notice.ts
export function renderNotice(target: HTMLElement, message: string) {
target.innerHTML = "";
const notice = document.createElement("p");
notice.setAttribute("role", "status");
notice.dataset.testid = "notice";
notice.textContent = message;
target.append(notice);
return notice;
}
// src/notice.test.ts
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { renderNotice } from "./notice";
afterEach(() => {
document.body.innerHTML = "";
});
describe("renderNotice", () => {
it("renders an accessible status message", () => {
document.body.innerHTML = '<div id="app"></div>';
const target = document.querySelector<HTMLDivElement>("#app");
if (!target) throw new Error("missing #app");
const notice = renderNotice(target, "Tersimpan");
expect(notice.getAttribute("role")).toBe("status");
expect(notice.textContent).toBe("Tersimpan");
expect({
html: document.body.innerHTML,
text: notice.textContent,
}).toMatchInlineSnapshot(`
{
"html": "<div id=\\"app\\"><p role=\\"status\\" data-testid=\\"notice\\">Tersimpan</p></div>",
"text": "Tersimpan",
}
`);
});
});
Cuplikan harus kecil. Atribut penting diperiksa langsung dengan expect; cuplikan hanya menyimpan struktur ringkas yang tidak boleh berubah tanpa alasan. Perilaku peramban nyata sebaiknya diuji dengan Playwright.
Cakupan dan CI sebagai Gerbang Kualitas
Cakupan bukan untuk mengejar angka, melainkan untuk menemukan cabang yang belum diuji. Vitest mendokumentasikan penyedia V8 dan Istanbul, dengan V8 sebagai bawaan. Tetapkan coverage.include; jika tidak, berkas baru yang tidak pernah diimport oleh pengujian bisa hilang dari laporan.
# .github/workflows/vitest.yml
name: vitest
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:run
- run: npm run coverage
Di CI, tulis vitest run secara eksplisit. vitest hanya memantau saat CI false dan terminal interaktif; di CI atau terminal noninteraktif perintah berjalan sekali. Untuk alat interaktif seperti lint-staged, gunakan vitest related --run. Desain alur yang lebih lengkap ada di panduan CI/CD Claude Code.
Prompt Praktis untuk Claude Code
Tambahkan pengujian Vitest untuk src/orders.ts.
Uji hanya createOrder.
Tirukan API eksternal dengan vi.fn(); jangan lakukan panggilan HTTP nyata.
Sertakan kasus sukses, input tidak valid, dan kegagalan transport.
Jangan gunakan timer palsu atau jsdom kecuali kode membutuhkannya.
Setelah mengedit, laporkan perintah npm run test:run yang diharapkan dan risiko tersisa.
Prompt ini memberikan ruang lingkup, batas tiruan, kasus gagal wajib, dan perintah bukti. Masukkan aturan serupa ke praktik terbaik CLAUDE.md agar sesi berikutnya tidak berhenti di jalur sukses saja.
Tiga kasus penggunaan
Kasus penggunaan 1: formulir pesanan atau pendaftaran. Jalur atau isi permintaan yang salah bisa menggagalkan pembelian. Claude Code dapat menulis pengujian batas API dengan vi.fn() untuk sukses, input tidak valid, dan kegagalan transport; manusia harus menyetujui pembayaran, penagihan, stok, dan aturan data pelanggan.
Kasus penggunaan 2: pengingat masa coba, janji, atau perpanjangan. Claude Code dapat membekukan Date, memajukan timer palsu, dan membuktikan callback berjalan sekali. Zona waktu, hari kerja, pengiriman email atau SMS nyata, serta dampak penagihan harus ditinjau manusia.
Kasus penggunaan 3: kontrak DOM kecil seperti notifikasi tersimpan, pesan validasi, atau banner admin. Claude Code dapat memeriksa teks, role, dan cuplikan kecil dengan jsdom. Perilaku peramban nyata, fokus, regresi visual, dan salinan yang terlihat pelanggan harus disetujui manusia.
Yang dikerjakan Claude Code dan yang disetujui manusia
| Area | Bisa didelegasikan ke Claude Code | Perlu persetujuan manusia |
|---|---|---|
| Kode uji | Tiruan, timer palsu, asersi jsdom, dan perintah cakupan | Pastikan asersi tidak melemahkan perilaku produk |
| CI | Menyarankan vitest run dan vitest run --coverage | Mengaktifkan check wajib, runner berbiaya, atau produksi |
| Data | Memakai ID sintetis seperti ord_1 dan book-1 | Meninjau data pelanggan, rahasia, API key, dan ID pembayaran |
| Peramban | Menjaga kontrak DOM di Vitest | Memvalidasi alur nyata dengan Playwright atau tinjauan manual |
Jebakan dan perbaikan
| Kegagalan | Gejala | Perbaikan |
|---|---|---|
| Tiruan tidak dipulihkan | Jumlah panggilan atau implementasi palsu bocor | Gunakan restoreMocks, vi.clearAllMocks(), atau vi.restoreAllMocks() sesuai tujuan |
| Timer palsu tidak dipulihkan | Pengujian waktu gagal di berkas lain | Panggil vi.useRealTimers() di afterEach |
| jsdom dianggap peramban nyata | CSS, layout, gambar, atau Canvas berbeda | Vitest untuk kontrak DOM, Playwright untuk perilaku peramban |
| Cuplikan terlalu besar | Tinjauan penuh gangguan | Simpan hanya struktur kecil |
coverage.include tidak ada | Berkas tanpa tes tidak terlihat | Sertakan src/**/*.{ts,tsx} secara jelas |
| Asinkron tidak ditunggu | Hasil positif palsu | Gunakan await expect(promise).resolves atau rejects |
| Runner interaktif masuk mode pantau | Proses menunggu perubahan | Gunakan vitest run; untuk berkas berubah gunakan vitest related --run |
Perbaikan bukan sekadar membuat hasil hijau. Saat pengujian gagal, tempelkan error lengkap, minta Claude Code tidak menghapus asersi, dan perbaiki implementasi atau pengaturan tiruan terlebih dahulu. Keamanan, produksi, penagihan, dan data pelanggan tetap menunggu manusia meninjau diff.
Untuk menerapkan aturan tinjauan yang sama di repository, gunakan template praktis ClaudeCodeLab untuk standar pengujian, prompt tinjauan, dan gerbang CI.
Hasil Verifikasi
Pada 22 Juli 2026, contoh TypeScript diekstrak ke proyek sementara dan dijalankan dengan Node v24.14.1 serta Vitest 4.1.10. vitest run dan vitest run --coverage meluluskan 4 berkas dan 7 pengujian untuk batas API, tiruan modul, jam tetap, batas timer, atribut DOM, dan cuplikan inline. Cakupan mencapai 94,73% statements, 100% branches, 85,71% functions, dan 94,73% lines. Mailer ditirukan, sehingga tidak ada email nyata atau API produksi yang dipanggil.
Artikel terkait
Utility Types TypeScript dengan Claude Code: panduan praktis
Pelajari Pick, Omit, Partial, Record, ReturnType, dan Awaited lewat contoh Claude Code yang bisa dijalankan.
Claude Agent SDK: masukkan Claude Code ke app dengan aman
Setup terbaru Claude Agent SDK, permission, MCP, contoh runnable, dan pitfall production.
Strategi Testing dengan Claude Code: Vitest, Testing Library, Playwright, dan CI
Rancang unit, integration, E2E, dan CI testing dengan Claude Code tanpa membuat test rapuh.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.