Menerapkan piramida testing untuk aplikasi fullstack: unit test untuk logika dan validasi, integration test untuk route handler dan query database, serta end-to-end test dengan Playwright yang menguji alur pengguna dari browser sampai database.

Kode tanpa test itu seperti berjalan di atas es tipis: aman sampai terdengar retakan. Episode ini membahas testing fullstack — bukan "cara menulis test", melainkan strategi test yang benar: apa yang diuji, di lapisan mana, dan berapa banyak. Karena aplikasi fullstack punya banyak lapisan, testing kalian juga harus tersusun berlapis.
Aturan yang akan kita pakai: piramida testing. Banyak test kecil dan cepat di bawah (unit), lebih sedikit test integration di tengah, dan sedikit test end-to-end yang lambat di puncak. Satu test end-to-end yang melindungi alur penting lebih berharga daripada sepuluh unit test yang menguji hal sepele.
| Lapisan | Contoh | Kecepatan | Jumlah |
|---|---|---|---|
| Unit | Validasi Zod, fungsi murni, komponen UI | Milidetik | Banyak |
| Integration | Route handler + database, server action | Detik | Sedang |
| E2E | Alur login → buat catatan → logout | Puluhan detik | Sedikit |
Prinsipnya: test murah untuk hal yang sering berubah, test mahal (E2E) hanya untuk alur kritis yang jarang berubah.
Kita pakai Vitest — test runner modern yang terintegrasi dengan Vite (fondasi Next.js). Install bersama Testing Library untuk menguji komponen:
pnpm add -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-domUnit test paling berharga untuk logika murni — dan schema Zod kita dari episode 7 adalah kandidat sempurna:
import { describe, expect, it } from "vitest"
import { noteSchema } from "./validations"
describe("noteSchema", () => {
it("menerima input valid", () => {
const result = noteSchema.safeParse({
title: "Catatan",
body: "Isi",
tags: ["kerja"],
})
expect(result.success).toBe(true)
})
it("menolak judul kosong", () => {
const result = noteSchema.safeParse({ title: "", body: "Isi", tags: [] })
expect(result.success).toBe(false)
})
it("menolak lebih dari 5 tag", () => {
const result = noteSchema.safeParse({
title: "x",
body: "y",
tags: ["1", "2", "3", "4", "5", "6"],
})
expect(result.success).toBe(false)
})
})Test ini bebas database dan network — jalan dalam milidetik dan memberi kalian kepercayaan penuh bahwa aturan validasi tidak berubah diam-diam.
Tip
Test validasi adalah test dengan ROI tertinggi di aplikasi web: input adalah gerbang semua data, dan aturannya (min/max/format) adalah tempat paling mudah regresi. Mulailah menulis test dari sini jika baru pertama kali.
Integration test memverifikasi lapisan yang lebih tebal: route handler memanggil database nyata. Kita memakai database test terpisah agar tidak mengotori data development:
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import { prisma } from "@/lib/prisma"
import { POST } from "./route"
beforeAll(async () => {
await prisma.note.deleteMany()
})
afterAll(async () => {
await prisma.note.deleteMany()
})
describe("POST /api/notes", () => {
it("membuat catatan valid dan mengembalikan 201", async () => {
const req = new Request("http://localhost/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Test", body: "Isi" }),
})
const res = await POST(req)
expect(res.status).toBe(201)
})
it("mengembalikan 400 untuk title kosong", async () => {
const req = new Request("http://localhost/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "", body: "Isi" }),
})
const res = await POST(req)
expect(res.status).toBe(400)
})
})Perhatikan: route handler diuji langsung sebagai fungsi — tanpa menjalankan server. Inilah keindahan integration test di framework fullstack: memanggil fungsi = memanggil endpoint.
Di puncak piramida, Playwright mengendalikan browser sungguhan dan menguji alur dari mata pengguna:
pnpm create playwright
pnpm exec playwright install chromiumimport { test, expect } from "@playwright/test"
test("pengguna bisa login dan membuat catatan", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("test@example.com")
await page.getByLabel("Password").fill("password-aman")
await page.getByRole("button", { name: "Masuk" }).click()
await expect(page).toHaveURL("/notes")
await page.goto("/notes/new")
await page.getByLabel("Judul").fill("Catatan pertama")
await page.getByLabel("Isi").fill("Dibuat lewat Playwright")
await page.getByRole("button", { name: "Simpan" }).click()
await expect(page).toHaveURL(/\/notes\/\w+/)
await expect(page.getByText("Catatan pertama")).toBeVisible()
})Test ini menguji seluruh tumpukan: UI, server action, validasi, database, redirect. Jika ada satu lapisan yang putus, test ini yang pertama menjerit.
Warning
E2E itu mahal dan rapuh (animasi, timing, browser). Batasi untuk alur kritis: login, checkout, alur data utama. Jangan menjadikan E2E pengganti unit test — itu seperti menguji jembatan hanya dengan meledakkannya sekali.
Selesaikan dengan urutan berikut:
validations.test.ts).pnpm vitest run
pnpm playwright testInti yang harus dibawa pulang:
Di episode 10 selanjutnya kita membahas styling & design integration — Tailwind, design tokens, dan konsistensi UI — agar aplikasi fullstack kalian tidak hanya bekerja, tapi juga terlihat profesional. Sampai jumpa di episode 10!