TDD (Test-Driven Development) artinya menulis test sebelum kodenya, lalu menulis kode sampai test itu lulus. CI (Continuous Integration) artinya server menjalankan semua test itu otomatis setiap kali ada push atau pull request. Bayangin penjahit. Dia bikin pola dari ukuran badanmu dulu, baru memotong kain. Selama potongannya belum pas dengan pola, bajunya belum jadi. Begitu pas, dia merapikan jahitannya, dan ukurannya gak boleh berubah. Pola itu test-nya.
🧪 Tebakan cepat: kamu menulis test untuk
hitungDiskonyang isinya masih kosong, lalu menjalankannya, dan test-nya langsung hijau. Kabar baik atau kabar buruk? Jawabannya ada di bagian "Red: lihat test-nya gagal dulu".
Siklus Red, Green, Refactor
TDD berjalan dalam putaran pendek, selalu dengan urutan ini:
- Red: tulis satu test untuk perilaku berikutnya, jalankan, dan lihat dia gagal (merah).
- Green: tulis kode paling sedikit yang bikin test itu lulus (hijau).
- Refactor: rapikan kodenya. Semua test harus tetap hijau.
Lalu ulang dari Red untuk perilaku berikutnya. Manfaat utamanya: tiap baris kode lahir untuk membuat sebuah test lulus, jadi kode lebih terjamin benar karena test-nya ditulis duluan. TDD gak membuat kode selesai lebih cepat, dan gak otomatis memberi cakupan test 100%.
Contoh di pelajaran ini dan di Lab: fungsi hitungDiskon(total) dengan aturan ini.
| Total belanja | Hasil | Contoh |
|---|---|---|
| di bawah 0 | 0 |
hitungDiskon(-100) jadi 0 |
| 0 sampai 100.000 | tanpa diskon | hitungDiskon(50000) jadi 50000 |
| di atas 100.000 sampai 500.000 | diskon 10% | hitungDiskon(200000) jadi 180000 |
| di atas 500.000 | diskon 20% | hitungDiskon(1000000) jadi 800000 |
Red: lihat test-nya gagal dulu
Putaran pertama: test untuk total negatif, dengan fungsi yang belum ada isinya.
// diskon.test.js
import { describe, it, expect } from "vitest";
import { hitungDiskon } from "./diskon";
describe("hitungDiskon", () => {
it("mengembalikan 0 untuk total negatif", () => {
expect(hitungDiskon(-100)).toBe(0);
});
});
// diskon.js
export function hitungDiskon(total) {}
npx vitest run menjawab:
× diskon.test.js > hitungDiskon > mengembalikan 0 untuk total negatif 3ms
→ expected undefined to be +0 // Object.is equality
Fungsi kosong mengembalikan undefined, jadi test-nya merah. Merah di sini kabar baik: terbukti test-nya bisa gagal. Test yang langsung hijau padahal kodenya kosong hampir pasti gak memeriksa apa-apa, biasanya karena expect-nya lupa ditulis. Itu jawaban tebakannya: kabar buruk.
Green: kode paling sedikit yang lulus
export function hitungDiskon(total) {
if (total < 0) return 0;
}
Cukup satu baris, dan test pertama hijau. Belum ada return total, karena belum ada test yang memintanya. Putaran berikutnya menambah test hitungDiskon(50000) jadi 50000 (merah, expected undefined to be 50000), lalu return total; (hijau). Begitu terus sampai keempat baris aturan punya test-nya. Di putaran diskon 20%, test-nya merah dengan expected 900000 to be 800000: kode masih memotong 10%.
Refactor: rapikan, test tetap hijau
Setelah keempat test hijau, kodenya dirapikan: angka ajaib diberi nama, urutan if dibuat jelas.
const BATAS_TANPA_DISKON = 100000;
const BATAS_DISKON_KECIL = 500000;
export function hitungDiskon(total) {
if (total < 0) return 0;
if (total <= BATAS_TANPA_DISKON) return total;
if (total <= BATAS_DISKON_KECIL) return total * 0.9;
return total * 0.8;
}
total < 0menangani input yang gak masuk akal duluan.<=bikin angka tepat di batas ikut tingkat yang lebih murah:hitungDiskon(100000)tetap100000, danhitungDiskon(500000)jadi450000(diskon 10%, bukan 20%).total * 0.9artinya potong 10%,total * 0.8potong 20%.
Syarat refactor cuma satu: perilaku gak berubah, jadi semua test tetap hijau. Hati-hati, keempat test tadi gak ada yang tepat di batas. Kalau <= gak sengaja jadi <, keempatnya tetap hijau, tapi hitungDiskon(500000) jadi 400000. Karena itu tambahkan test untuk 100.000 dan 500.000.
Latihan: test dulu, lalu kodenya
Latihan pertama memakai hitungDiskon di atas. Fungsi runTddTests() di kode awal sudah membandingkan empat kasus dan mencetak jumlah yang lulus, jadi kalau semuanya benar, console menulis 4.
Dua latihan berikutnya memakai isPalindrome(str): true kalau teksnya sama saat dibaca terbalik, misalnya "katak".
function isPalindrome(str) {
const teks = str.toLowerCase();
return teks === teks.split("").reverse().join("");
}
describe("isPalindrome", () => {
it("true untuk kata palindrom", () => {
expect(isPalindrome("katak")).toBe(true);
});
it("false untuk kata biasa", () => {
expect(isPalindrome("hello")).toBe(false);
});
});
toLowerCase()menyamakan huruf, jadi"Malam"juga terhitung palindrom.split("")memecah teks jadi huruf,reverse()membalik urutannya,join("")menyatukannya lagi.expect(isPalindrome(...))memeriksa hasil pemanggilan fungsinya, bukan nilai yang diketik langsung.- Test kedua memeriksa kasus yang harus ditolak (
toBe(false)). Test yang cuma membuktikan kasus benar belum membuktikan banyak: fungsi yang selalu menjawabtruejuga lulus. Kata biasa seperti"belajar"juga harus dijawabfalse.
CI: test jalan sendiri di tiap push
CI singkatan dari Continuous Integration: kode dari banyak developer digabung sesering mungkin, dan tiap penggabungan dicek otomatis. CD (Continuous Delivery atau Deployment) melanjutkannya: kode yang lulus langsung siap dirilis atau langsung dirilis. Di GitHub, alatnya GitHub Actions: sebuah file YAML di .github/workflows/.
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main]
pull_request:
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: npx vitest run
on: pushdanon: pull_requestadalah pemicunya: test jalan otomatis tiap ada push atau pull request kemain, gak perlu ada yang menjalankannya manual.npm cimemasang dependency persis sesuaipackage-lock.json.- Kalau satu test gagal,
vitest runkeluar dengan kode 1, dan job-nya merah.
Dengan branch protection di pengaturan repo, pull request yang test-nya merah gak bisa di-merge.
⚠️ Jebakan yang sering ketemu
- Kode dulu, test belakangan. Urutan "tulis kode, tulis test, jalankan, refactor" bukan TDD. Di TDD, test ditulis dulu dan dilihat merah dulu.
- Test tanpa
expect. Langsung hijau walau kodenya kosong. Kalau test baru gak pernah merah, curigai test-nya. - Salah sisi di batas.
<dan<=di 100.000 dan 500.000 menentukan hasilnya. Selalu tulis test tepat di batas. - Refactor yang mengubah perilaku. Kalau ada test yang patah waktu refactor, itu bukan refactor, itu bug baru.
TL;DR: TDD berputar Red, Green, Refactor: tulis test dan lihat dia gagal, tulis kode paling sedikit sampai hijau, lalu rapikan dengan semua test tetap hijau. Merah duluan membuktikan test-nya bisa gagal, dan manfaat utamanya: kode lebih terjamin benar karena test-nya ada lebih dulu. Untuk hitungDiskon: di bawah 0 jadi 0, sampai 100.000 tanpa diskon, sampai 500.000 diskon 10%, di atasnya 20%, dan angka tepat di batas ikut tingkat yang lebih murah (<=). CI (Continuous Integration) menjalankan test otomatis di tiap push atau pull request, di GitHub lewat GitHub Actions, dan branch protection menolak merge yang test-nya merah.