Test API adalah kode yang mengirim request ke aplikasimu lalu mengecek jawabannya, otomatis, tiap kali kamu mengubah kode. Bayangin pelanggan rahasia di restoran. Tiap kali dapur ganti resep, dia datang lagi, memesan menu yang sama, dan mencentang daftarnya: pesanannya datang? Isinya benar? Kalau dia minta menu yang gak ada, pelayan bilang "gak ada", bukan diam? Di Node, pelanggan itu supertest (mengirim request), dan daftar centangnya ditulis dengan Vitest (describe, it, expect).
🧪 Tebakan cepat: di route
POST /catatan, seseorang menghapus.status(201)sehingga tinggalres.json(baru). Test yang mengecekexpect(res.status).toBe(201)jadi apa, dan pesannya bilang apa? Tebak dulu, jawabannya ada di bagian "Kalau ada yang rusak: test yang merah".
Siapkan: app.js tanpa listen
Supertest butuh object app-nya, bukan server yang sudah menyala di port 3000. Jadi catatan-api dipecah dua: app.js membuat dan meng-export app, server.js yang menyalakannya.
// app.js
const express = require("express");
const app = express();
app.use(express.json());
let catatan = [];
let nextId = 3;
function reset() {
catatan = [
{ id: 1, judul: "Belanja", isi: "beli beras 5 kg" },
{ id: 2, judul: "Belajar", isi: "bab Node.js" },
];
nextId = 3;
}
reset();
// GET /catatan, GET /catatan/:id (404 kalau gak ada),
// POST /catatan (400 kalau judul kosong, 201 kalau berhasil)
module.exports = { app, reset };
// server.js
const { app } = require("./app");
app.listen(3000, () => console.log("Server jalan di http://localhost:3000"));
Pasang alatnya dengan npm install -D vitest supertest, lalu di package.json tambahkan "scripts": { "test": "vitest run" }. -D artinya cuma dipakai saat development.
Test pertama: describe, it, expect
// test/catatan.test.js
import { describe, it, expect, beforeEach } from "vitest";
import request from "supertest";
import { app, reset } from "../app.js";
beforeEach(() => {
reset(); // tiap test mulai dari dua catatan yang sama
});
describe("GET /catatan", () => {
it("mengirim semua catatan", async () => {
const res = await request(app).get("/catatan");
expect(res.status).toBe(200);
expect(res.body.length).toBe(2);
});
});
describe("GET /catatan", ...)mengelompokkan test yang saling terkait jadi satu test suite. Namanya ikut dicetak di hasil.it("...", ...)adalah satu kasus uji. Satuit= satu hal yang dicek.request(app).get("/catatan")mengirim GET keapptanpa membuka port. Ada juga.post(...),.put(...),.delete(...), dan.send({...})untuk body JSON.expect(nilai).toBe(harapan)melempar error kalau nilainya beda, dan error itulah yang bikin test gagal.- Supertest juga bisa mengecek status langsung:
await request(app).get("/catatan").expect(200);.
Vitest bukan satu-satunya: Jest dan Mocha juga populer di Node, dan cara menulisnya mirip.
Jalur sukses dan jalur gagal
Test yang baik bukan cuma mencoba yang lancar. Untuk tiap route, cek juga permintaan yang salah:
it("menjawab 404 untuk catatan yang gak ada", async () => {
const res = await request(app).get("/catatan/99");
expect(res.status).toBe(404);
expect(res.body.error).toBe("Catatan tidak ditemukan");
});
describe("POST /catatan", () => {
it("membuat catatan baru", async () => {
const res = await request(app).post("/catatan").send({ judul: "Olahraga", isi: "lari 5 km" });
expect(res.status).toBe(201);
expect(res.body.id).toBe(3);
});
it("menolak judul kosong", async () => {
const res = await request(app).post("/catatan").send({ judul: "", isi: "x" });
expect(res.status).toBe(400);
});
});
Empat test, empat janji API-mu: daftar = 200 berisi 2, id yang gak ada = 404, catatan baru = 201 dengan id 3, judul kosong = 400. Jalankan dengan npm test:
✓ test/catatan.test.js (4 tests) 27ms
Test Files 1 passed (1)
Tests 4 passed (4)
Kalau ada yang rusak: test yang merah
Hapus .status(201) di route POST, lalu npm test lagi (dipotong):
❯ test/catatan.test.js (4 tests | 1 failed) 27ms
✓ GET /catatan > mengirim semua catatan 12ms
✓ GET /catatan > menjawab 404 untuk catatan yang gak ada 3ms
× POST /catatan > membuat catatan baru 9ms
→ expected 200 to be 201 // Object.is equality
✓ POST /catatan > menolak judul kosong 2ms
Test Files 1 failed (1)
Tests 1 failed | 3 passed (4)
res.json(...) tanpa .status(...) menjawab 200, jadi test-nya gagal dengan expected 200 to be 201: yang diterima 200, yang diharapkan 201. Itu jawaban tebakannya. Tiga test lain tetap hijau, jadi kamu langsung tahu route mana yang rusak, sebelum pengguna yang menemukannya.
beforeEach: tiap test mulai dari data yang sama
Test POST menambah catatan ketiga. Tanpa reset() di beforeEach, test yang jalan sesudahnya melihat 3 catatan, dan expect(res.body.length).toBe(2) gagal dengan expected 3 to be 2, padahal route-nya benar. Test yang hasilnya bergantung pada urutan bikin bingung, jadi tiap test harus mulai dari keadaan yang sama.
Mock: database tiruan
Kalau data diambil dari database sungguhan, test jadi lambat (harus konek), hasilnya bisa berubah (isi database berubah), dan test saling mengganggu. Solusinya mock: fungsi tiruan yang jawabannya kamu tentukan.
// catatanService.js: db dikirim dari luar, jadi bisa diganti saat test
async function catatanBerjudul(db) {
const semua = await db.ambilSemua(); // di production: query ke database
return semua.filter((c) => c.judul !== "");
}
// test
import { it, expect, vi } from "vitest";
it("membuang catatan tanpa judul", async () => {
const dbPalsu = { ambilSemua: vi.fn().mockResolvedValue([{ id: 1, judul: "Belanja" }, { id: 2, judul: "" }]) };
const hasil = await catatanBerjudul(dbPalsu);
expect(hasil.length).toBe(1);
expect(dbPalsu.ambilSemua).toHaveBeenCalledTimes(1);
});
vi.fn() membuat fungsi tiruan yang mencatat tiap panggilannya, dan .mockResolvedValue(...) membuatnya mengembalikan Promise berisi data itu. Hasilnya: test cepat dan deterministik (jawabannya selalu sama), dan jalur error bisa dicoba dengan .mockRejectedValue(new Error("koneksi putus")).
Di latihan: test runner mini
Di balik it sebenarnya cuma try/catch: jalankan fungsinya, kalau ada error yang dilempar, test itu gagal, dan program jalan terus ke test berikutnya. Di latihan kamu menulisnya sendiri: test(name, fn) mencetak PASS: <name> atau FAIL: <name>, assert(cond) melempar Error kalau cond salah, dan runTests() menghitung hasilnya:
PASS: 1 + 1 sama dengan 2
PASS: array includes item
FAIL: string length is wrong (intentional fail)
Results: 2 passed, 1 failed
⚠️ Jebakan yang sering ketemu
app.listendi dalamapp.js. Tiap file test menyalakan server di port 3000, dan yang kedua gagal denganEADDRINUSE. Pisahkanserver.js.- Lupa
await.const res = request(app).get(...)tanpaawaitberisi request yang belum selesai, jadires.statusundefined. - Cuma mengetes jalur sukses. Bug paling sering ada di 400, 404, dan 401. Tiap route minimal punya satu test untuk permintaan yang salah.
- Test saling berbagi data. Reset data di
beforeEach, jangan andalkan urutan test.
TL;DR: Pisahkan app.js (export app) dari server.js (listen), pasang vitest dan supertest, lalu tulis test: describe mengelompokkan test, it satu kasus, const res = await request(app).get("/catatan") mengirim request tanpa membuka port, dan expect(res.status).toBe(200) mengecek hasilnya (atau .expect(200) dari supertest). Uji jalur sukses dan jalur gagal (201, 400, 404). Test yang gagal mencetak expected 200 to be 201: diterima 200, diharapkan 201. beforeEach mengembalikan data ke awal, dan mock (vi.fn().mockResolvedValue(...)) mengganti database supaya test cepat dan deterministik. Di balik it cuma try/catch.