Test-Driven Development: Tulis Test Terlebih Dahulu

Foto oleh Unsplash

Foto oleh Unsplash
Test-Driven Development (TDD) adalah disiplin di mana Anda menulis test yang gagal sebelum menulis satu baris kode produksi. Siklus Red-Green-Refactor — tulis test yang gagal, buat berhasil dengan kode minimal, lalu refactor tanpa merusak test — terdengar kontra-intuitif pada awalnya, tetapi menghasilkan kode yang benar-benar dapat ditest by design, memiliki tingkat cacat yang jauh lebih rendah, dan jauh lebih mudah untuk di-refactor dengan aman. Postingan ini mengajarkan TDD dengan contoh TypeScript dan Jest nyata yang bisa Anda ikuti.
TDD beroperasi dalam tiga fase. Red: tulis test yang mendeskripsikan perilaku yang ingin Anda tambahkan. Test harus gagal — jika berhasil tanpa kode baru, Anda telah menulis test yang buruk atau fitur sudah ada. Green: tulis kode minimum yang diperlukan untuk membuat test berhasil — tidak lebih. Tahan godaan untuk menambahkan logika ekstra. Refactor: bersihkan kode produksi dan test dengan percaya diri, mengetahui bahwa test suite Anda akan menangkap setiap regresi.
Mari kita bangun shopping cart dengan TDD. Kita mulai dengan test sesederhana mungkin — bisakah kita menambahkan item dan menghitung total? — dan menumbuhkan implementasi dari sana. Setiap test mendorong perilaku baru. Perhatikan bagaimana kelas Cart akhir secara alami kecil dan terfokus: TDD mencegah over-engineering dengan memaksa Anda hanya mengimplementasikan yang ditest.
// Step 1 — RED: write a failing test
// src/cart/cart.test.ts
import { describe, it, expect } from "vitest";
import { Cart } from "./cart";
describe("Cart", () => {
it("calculates total with quantity", () => {
const cart = new Cart();
cart.addItem({ id: "1", name: "Widget", price: 10, qty: 3 });
expect(cart.total()).toBe(30);
});
it("applies 10% discount when total exceeds 100", () => {
const cart = new Cart();
cart.addItem({ id: "1", name: "Widget", price: 50, qty: 3 });
expect(cart.total()).toBe(135); // 150 - 10%
});
});
// Step 2 — GREEN: write the minimal implementation
// src/cart/cart.ts
export class Cart {
private items: CartItem[] = [];
addItem(item: CartItem) { this.items.push(item); }
total(): number {
const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
return subtotal > 100 ? subtotal * 0.9 : subtotal;
}
}
// Step 3 — REFACTOR: clean up without breaking tests
export class Cart {
private items: CartItem[] = [];
addItem(item: CartItem) { this.items.push(item); }
private subtotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
private applyBulkDiscount(amount: number): number {
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.1;
return amount > DISCOUNT_THRESHOLD ? amount * (1 - DISCOUNT_RATE) : amount;
}
total(): number {
return this.applyBulkDiscount(this.subtotal());
}
}Saat mengikuti TDD, commit setelah setiap fase Green — sebelum refactoring. Ini memberi Anda titik rollback yang bersih jika refactoring berjalan melenceng, dan menunjukkan progress inkremental TDD kepada tim Anda dalam git history.
Aplikasi nyata memiliki dependency eksternal: database, layanan email, payment gateway. TDD mengharuskan Anda menguji unit secara terisolasi, yang berarti mengganti dependency ini dengan test double. Jest's vi.mock() (atau jest.mock()) memungkinkan Anda mengganti modul dengan fake yang merekam panggilannya, memungkinkan Anda memverifikasi perilaku tanpa menyentuh infrastruktur nyata.
Mock dependency eksternal (HTTP call, layanan email, payment processor) karena lambat, tidak stabil, dan mahal dalam test. Gunakan implementasi nyata untuk logika bisnis murni dan transformasi data. Gunakan in-memory database (seperti better-sqlite3 atau test Postgres instance) untuk test yang benar-benar perlu memverifikasi persistensi data — mocking layer database dapat memberikan kepercayaan palsu jika query Anda salah.
Strukturkan setiap test dengan tiga bagian: Arrange (set up prasyarat — buat instance, mock dependency, seed data), Act (jalankan satu perilaku yang ditest), dan Assert (verifikasi hasilnya). Jaga agar setiap test fokus pada satu perilaku. Ketika sebuah test memiliki beberapa pasangan Act/Assert, ia menguji terlalu banyak hal dan akan memberi Anda diagnostik kegagalan yang buruk.
// Mocking external dependencies in Jest/Vitest
import { vi } from "vitest";
import { sendWelcomeEmail } from "./emailService";
import { UserService } from "./userService";
vi.mock("./emailService");
describe("UserService.register", () => {
it("sends a welcome email after successful registration", async () => {
const service = new UserService({ db: mockDb });
await service.register({ email: "test@example.com", name: "Alice" });
expect(sendWelcomeEmail).toHaveBeenCalledWith(
expect.objectContaining({ email: "test@example.com" })
);
});
it("throws when email already exists", async () => {
mockDb.findByEmail.mockResolvedValueOnce({ id: "existing" });
const service = new UserService({ db: mockDb });
await expect(service.register({ email: "dup@example.com", name: "Bob" }))
.rejects.toThrow("Email already registered");
});
});TDD tidak berlaku secara universal. Memahami di mana ia memberikan nilai terbesar dan di mana ia menambah gesekan membantu Anda menerapkannya secara pragmatis daripada dogmatis.
TDD paling berharga untuk: logika bisnis dengan aturan kompleks (kalkulasi pajak, discount engine, logika validasi), kode yang akan sering di-refactor, perbaikan bug (tulis test yang mereproduksi bug terlebih dahulu, lalu perbaiki — test memastikan bug tidak pernah kembali), dan API dengan kontrak yang terdefinisi dengan baik. Dalam konteks ini, test suite menjadi dokumentasi yang dapat dieksekusi yang selalu terkini dengan kode.
TDD bekerja buruk untuk pemrograman eksploratoris (ketika Anda belum tahu desain yang tepat), komponen UI kompleks di mana output yang 'benar' bersifat visual dan subjektif, dan skrip sekali pakai yang tidak akan pernah dipelihara. Memaksakan TDD pada konteks ini menciptakan test yang sangat terikat pada detail implementasi dan rusak terus-menerus dengan setiap perubahan — kebalikan dari stabilitas yang dimaksudkan TDD untuk disediakan.
100% code coverage bukan tujuan TDD. Coverage adalah lagging indicator — ini memberi tahu Anda kode apa yang dieksekusi, bukan apakah test bermakna. Sangat mungkin memiliki 100% coverage tanpa assertion. Sebaliknya, targetkan coverage tinggi pada jalur kritis bisnis, dan perlakukan coverage rendah pada edge case sebagai sinyal risiko yang layak diselidiki.
Menyiapkan TDD dalam proyek TypeScript memerlukan pemilihan test runner (Jest dengan ts-jest atau Vitest native), mengonfigurasi path alias agar sesuai dengan tsconfig Anda, menyiapkan pelaporan code coverage, dan mengintegrasikan test Anda ke dalam pipeline CI sehingga setiap pull request diverifikasi sebelum merge.
Vitest adalah pilihan modern untuk proyek TypeScript, terutama yang menggunakan Vite atau Next.js 15. Ia menggunakan konfigurasi yang sama dengan Vite, mendukung ESM secara native, dan memiliki watch mode yang jauh lebih cepat daripada Jest. Untuk Jest, gunakan ts-jest untuk kompilasi TypeScript langsung atau konfigurasikan Jest dengan Babel dan @babel/preset-typescript. Keduanya mendukung watch mode, coverage, dan snapshot testing.
Letakkan unit test berdampingan dengan kode yang ditest (cart.ts berdampingan dengan cart.test.ts). Simpan integration test dalam direktori __tests__ terpisah atau folder /tests/integration khusus. Gunakan blok describe untuk mengelompokkan perilaku terkait. Gunakan it.todo() untuk mendokumentasikan test yang direncanakan. Jalankan unit test saat file disimpan (mode --watch) dan test integrasi penuh di CI.
Gunakan mode 'ui' Vitest (npx vitest --ui) untuk test runner berbasis browser dengan visualisasi pass/fail real-time. Ini secara dramatis mempercepat siklus TDD saat mengerjakan logika bisnis yang kompleks.
TDD bukan hanya praktik teknis — ini adalah budaya tim. Mulai dengan kesepakatan tim tentang ambang coverage minimum (80% untuk kode baru adalah floor pragmatis yang umum). Wajibkan test dalam pull request. Rayakan perbaikan bug yang didorong test. Jadikan test sebagai warga kelas satu dalam code review, memberikan mereka pengawasan yang sama dengan kode produksi. Test suite yang baik adalah aset tim yang menghemat berjam-jam debugging setiap sprint.
Konsep testing kunci dalam postingan ini meliputi TDD, Red-Green-Refactor, mock, coverage, and AAA pattern.