TDD & CI/CD Automation — Testing

Test-Driven Development (TDD) TDD adalah metodologi pengembangan di mana kita menulis test sebelum menulis kode implementasi. Prosesnya mengikuti siklus…

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 hitungDiskon yang 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:

  1. Red: tulis satu test untuk perilaku berikutnya, jalankan, dan lihat dia gagal (merah).
  2. Green: tulis kode paling sedikit yang bikin test itu lulus (hijau).
  3. 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;
}

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);
  });
});

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

Dengan branch protection di pengaturan repo, pull request yang test-nya merah gak bisa di-merge.

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari