Test adalah kode yang memakai komponenmu seperti pembeli memakainya, lalu memeriksa hasilnya secara otomatis. Bayangin Bu Sri punya pembeli percobaan. Tiap pagi sebelum warung buka, dia memesan hal yang sama, menekan tombol yang sama, dan mencocokkan struknya. Kalau angkanya meleset, dia langsung lapor, sebelum pembeli sungguhan kena. Vitest adalah yang mengirim pembeli percobaan dan menulis laporannya, Testing Library adalah tangan dan matanya, dan jsdom adalah warung tiruan tempat dia mencoba.
🧪 Tebakan cepat: tes "dua klik +1 jadi Count: 2" dijalankan pada Counter yang tombolnya menulis
count = 1, bukancount += 1. Tes itu lulus atau gagal, dan apa yang dilaporkan tentang halamannya? Jawabannya ada di bagian "Saat tes gagal".
Pasang Vitest dan Testing Library
Di proyek SvelteKit Warung Bu Sri:
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
// vite.config.js (potongan)
import { svelteTesting } from "@testing-library/svelte/vite"
export default defineConfig({
plugins: [sveltekit(), svelteTesting()],
test: {
environment: "jsdom",
setupFiles: ["./vitest-setup.js"],
},
})
- Vitest adalah test runner: dia mencari file
*.test.js, menjalankan tiap tes, dan mencetak laporannya. - @testing-library/svelte memasang komponen (
render) dan mencarinya seperti pengguna mencari (screen.getByRole,screen.getByText);svelteTesting()menyiapkan Svelte versi browser untuknya. - jsdom adalah DOM tiruan yang jalan di Node, dan jest-dom menambah pemeriksaan seperti
toBeInTheDocument(). Filevitest-setup.jscukup berisi satu baris:import "@testing-library/jest-dom/vitest".
Tes pertama: render dan getByRole
Komponennya sama dengan latihan:
<!-- src/lib/Counter.svelte -->
<script>
let count = 0
</script>
<p>Count: {count}</p>
<button on:click={() => (count += 1)}>+1</button>
// src/lib/Counter.test.js
import { render, screen, fireEvent } from "@testing-library/svelte"
import { describe, it, expect } from "vitest"
import Counter from "./Counter.svelte"
describe("Counter", () => {
it("mulai dari Count: 0", () => {
render(Counter)
expect(screen.getByText("Count: 0")).toBeInTheDocument()
})
it("dua klik +1 jadi Count: 2", async () => {
render(Counter)
const tombol = screen.getByRole("button", { name: "+1" })
await fireEvent.click(tombol)
await fireEvent.click(tombol)
expect(screen.getByText("Count: 2")).toBeInTheDocument()
})
})
describemengelompokkan tes,itadalah satu tes,expectpemeriksaannya.render(Counter)memasang komponen ke DOM tiruan. Props dikirim begini:render(MenuItem, { props: { nama: "Mie Ayam", harga: 12000 } }).screen.getByText("Count: 0")mencari elemen yang teksnya persis itu, dan melempar error kalau gak ada.screen.getByRole("button", { name: "+1" })mencari tombol lewat perannya dan tulisannya, seperti pembaca layar. Tesnya membaca apa yang dilihat pengguna, bukan nama variabel atau class.await fireEvent.click(tombol)mengklik tombolnya dan menunggu Svelte selesai memperbarui DOM.
Dijalankan di proyek SvelteKit 3 sungguhan dengan npx vitest run --reporter=verbose (--reporter=verbose mencetak nama tiap tes; ini potongannya):
✓ src/lib/Counter.test.js > Counter > mulai dari Count: 0 26ms
✓ src/lib/Counter.test.js > Counter > dua klik +1 jadi Count: 2 59ms
Tests 2 passed (2)
Store dan action: tanpa render
Store dan action adalah JavaScript biasa, jadi dites langsung tanpa komponen:
// src/lib/keranjang.test.js
import { get } from "svelte/store"
import { describe, it, expect, beforeEach } from "vitest"
import { keranjang } from "./keranjang.js"
describe("keranjang", () => {
beforeEach(() => keranjang.kosongkan())
it("menu yang sama dua kali jadi 2 porsi", () => {
keranjang.tambah("Mie Ayam")
keranjang.tambah("Mie Ayam")
expect(get(keranjang)).toEqual([{ nama: "Mie Ayam", porsi: 2 }])
})
})
// src/routes/pesan/page.server.test.js
import { describe, it, expect } from "vitest"
import { actions } from "./+page.server.js"
function kirim(isian) {
const request = new Request("http://localhost/pesan", { method: "POST", body: new URLSearchParams(isian) })
return actions.default({ request })
}
describe("action /pesan", () => {
it("nama kosong: 400, isiannya dikembalikan", async () => {
const hasil = await kirim({ nama: "", menu: "2", porsi: "3" })
expect(hasil.status).toBe(400)
expect(hasil.data).toEqual({ nama: "", menuId: 2, porsi: 3, pesan: "Nama pemesan wajib diisi" })
})
it("pesanan benar: redirect 303 ke terima kasih", async () => {
await expect(kirim({ nama: "Andi", menu: "2", porsi: "3" })).rejects.toMatchObject({
status: 303,
location: "/pesan/terima-kasih",
})
})
})
get(keranjang)darisvelte/storemembaca isi store sekali.beforeEachmengosongkannya sebelum tiap tes, supaya tes gak saling mempengaruhi.- Action dipanggil seperti function biasa dengan
Requestbuatan.fail(...)mengembalikan objek denganstatusdandata;redirect(...)melempar, jadi diperiksa denganrejects. - Dijalankan bersama
Counter.test.js, ketiga file lulus:Test Files 3 passed (3).console.logdari action ikut tercetak sebagai barisstdout:pesanan masuk: Andi Mie Ayam 3 porsi.
Saat tes gagal
Tombolnya diubah jadi count = 1. Tes pertama tetap lulus, tes kedua gagal, dan laporannya mencetak DOM saat tes menyerah. Itu jawaban tebakannya. Ini bagian atas laporannya (bagian FAIL di bawahnya mengulang pesan yang sama dan menunjuk barisnya di Counter.test.js):
✓ src/lib/Counter.test.js > Counter > mulai dari Count: 0 71ms
× src/lib/Counter.test.js > Counter > dua klik +1 jadi Count: 2 112ms
→ Unable to find an element with the text: Count: 2. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<p>
Count: 1
</p>
<button>
+1
</button>
</div>
</body>
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Baca dari atas: tes mana yang gagal (×), apa yang dicari (Count: 2), lalu apa yang sebenarnya ada (Count: 1). Klik kedua gak menambah lagi, jadi bug-nya ada di handler tombol.
userEvent: klik seperti manusia
@testing-library/user-event (pasang dengan npm install -D @testing-library/user-event) mengklik seperti pengguna sungguhan: pointer turun, fokus, pointer naik, lalu klik. Semua method-nya async:
import userEvent from "@testing-library/user-event"
const user = userEvent.setup()
render(Counter)
await user.click(screen.getByRole("button", { name: "+1" }))
Tanpa await, tes memeriksa sebelum kliknya terjadi. Dicoba: user.click(tombol) dua kali tanpa await gagal, dan DOM yang dicetak masih Count: 0. Dengan fireEvent.click di Svelte 5, tes yang lupa await kebetulan masih lulus; tetap tulis await di keduanya.
Proyek baru: SvelteKit 3 dan Svelte 5
Di proyek dari npx sv create, Counter-nya ditulis let count = $state(0) dan onclick, dan Counter.test.js yang sama lulus tanpa diubah satu baris pun, karena tes cuma membaca DOM. Komponen gaya Svelte 4 di proyek itu butuh <svelte:options runes={false} /> di baris pertamanya, karena runes dinyalakan untuk semua file.
Latihan: menulis tes Counter
Editor latihan punya test runner kecil yang meniru Vitest dan @testing-library/svelte: describe, it, expect dengan matcher yang umum (termasuk toBeInTheDocument), render, screen.getByText, screen.getByRole, fireEvent, dan userEvent. Editornya berisi Counter.svelte di atas dan, mulai baris // Counter.test.js, tes-mu. Pratinjau menampilkan laporan seperti Vitest: ✓ atau × per tes, lalu Tests 2 passed (2).
Setelah semua tes lulus, editor menjalankan tesmu lagi pada salinan Counter.svelte yang sengaja dirusak sedikit, misalnya let count = 1 atau count -= 1. Cara ini namanya mutation testing, seperti alat Stryker. Tes yang bagus gagal pada setiap salinan rusak (killed). Salinan yang lolos (survived) menunjukkan hal yang belum dicek tesmu: tes yang cuma membaca Count: 0 meloloskan count -= 1, jadi periksa juga Count: 2 setelah dua klik.
⚠️ Jebakan yang sering ketemu
- Mencari sebagian teks.
getByText("Count")gagal di<p>Count: 0</p>:Unable to find an element with the text: Count. Secara bawaan teksnya harus sama persis. - Nama tombol beda dengan tes. Tombol bertuliskan
TambahmembuatgetByRole("button", { name: "+1" })gagal, dan laporannya mendaftar peran yang ada:Name "Tambah". - Lupa
awaitdi userEvent. Pemeriksaannya jalan sebelum kliknya terjadi. - Menguji action yang redirect tanpa
rejects.redirect()melempar, jadiawait kirim(...)biasa langsung gagal dengan{ status: 303, location: '/pesan/terima-kasih' }.
TL;DR: Tes Svelte memakai Vitest sebagai test runner, @testing-library/svelte untuk render dan mencari elemen, dan jsdom sebagai DOM tiruan; svelteTesting() di vite.config.js dan environment: "jsdom" menyiapkannya. render(Counter) memasang komponen (props lewat { props: {...} }), screen.getByText("Count: 0") dan screen.getByRole("button", { name: "+1" }) mencarinya seperti pengguna, await fireEvent.click(...) atau await user.click(...) dari userEvent mengklik, dan expect(...).toBeInTheDocument() memeriksa. Store dites tanpa render dengan get(store), action dipanggil dengan Request buatan dan redirect diperiksa lewat rejects. Laporan yang gagal mencetak apa yang dicari dan DOM yang sebenarnya. Tes membaca DOM, jadi tes yang sama lulus untuk komponen Svelte 4 dan runes. Latihannya: menulis dua tes untuk Counter, Count: 0 di awal dan Count: 2 setelah dua klik, lalu mutation testing memastikan tesnya menangkap salinan yang rusak.