1. Pengenalan Testing & Jest
Unit testing adalah praktik menguji bagian-bagian kecil dari kode (fungsi, metode, komponen) secara terpisah untuk memastikan mereka berfungsi sesuai harapan. Testing adalah bagian penting dari pengembangan software profesional — ia mencegah bug, memudahkan refactoring, dan mendokumentasikan perilaku kode.
Jest adalah framework testing JavaScript yang dikembangkan oleh Meta (Facebook). Jest populer karena konfigurasi nol, API yang intuitif, built-in assertion, mocking, dan code coverage. Jest digunakan secara luas di proyek React, Node.js, dan ekosistem JavaScript lainnya.
Mengapa Testing Penting?
| Manfaat | Penjelasan |
|---|---|
| Cegah Bug | Detect kesalahan sebelum kode sampai ke production |
| Refactoring Aman | Ubah kode dengan percaya diri — jika test gagal, Anda tahu apa yang rusak |
| Dokumentasi Hidup | Test mendokumentasikan bagaimana kode seharusnya bekerja |
| Desain Lebih Baik | Kode yang mudah di-test biasanya lebih modular dan bersih |
| Collaboration | Tim bisa bekerja lebih cepat karena punya safety net otomatis |
| Deploy Percaya Diri | CI/CD dengan test otomatis = deploy tanpa takut |
Jenis-jenis Testing
| Jenis | Cakupan | Kecepatan | Contoh |
|---|---|---|---|
| Unit Test | 1 fungsi/1 komponen | 🟢 Sangat cepat | Test fungsi kalkulasi |
| Integration Test | Beberapa modul bersama | 🟡 Sedang | Test API + database |
| End-to-End (E2E) | Seluruh aplikasi | 🔴 Lambat | Test flow login sampai checkout |
Instalasi Jest
# Instal Jest sebagai dev dependency
npm install --save-dev jest
# Atau dengan yarn
yarn add --dev jest
# Tambahkan script di package.json:
# "scripts": {
# "test": "jest",
# "test:watch": "jest --watch",
# "test:coverage": "jest --coverage"
# }
# Jalankan test
npm test
# Jalankan test dalam mode watch (otomatis re-run saat file berubah)
npm run test:watch
Konvensi Penamaan File Test
# Konvensi penamaan file test di Jest: # # src/ # ├── utils/ # │ ├── math.js ← Kode sumber # │ ├── math.test.js ← Test file (berdampingan) # │ └── __tests__/ # │ └── math.test.js ← Atau di folder __tests__ # ├── components/ # │ ├── Button.jsx # │ └── Button.test.jsx # └── services/ # ├── api.js # └── api.test.js # # Jest otomatis menemukan file yang: # - Berakhiran .test.js / .test.ts / .test.jsx / .test.tsx # - Berakhiran .spec.js / .spec.ts / .spec.jsx / .spec.tsx # - Berada di dalam folder __tests__/
┌───────────────────────────────────────────────────────┐
│ ANATOMI SEBUAH TEST │
│ │
│ describe("Kalkulator", () => { ← Test Suite │
│ │
│ test("menjumlahkan 2 angka", () => { ← Test Case │
│ │
│ const hasil = tambah(2, 3); ← Arrange │
│ (Siapkan data) │
│ │
│ expect(hasil).toBe(5); ← Act & Assert │
│ (Jalankan & │
│ Verifikasi) │
│ }); │
│ │
│ test("membagi 2 angka", () => { ← Test Case │
│ expect(bagi(10, 2)).toBe(5); │
│ }); │
│ │
│ }); │
│ │
│ describe = Grup test berdasarkan topik │
│ test/it = Kasus uji individual │
│ expect = Assertion (klaim yang divalidasi) │
└───────────────────────────────────────────────────────┘
2. Unit Testing Dasar
Unit test menguji satu fungsi atau satu unit logika secara terpisah. Ini adalah jenis test yang paling banyak dan paling cepat. Mari kita mulai dengan contoh sederhana.
Contoh Pertama: Test Fungsi Dasar
// math.js — Kode yang akan di-test
function tambah(a, b) {
return a + b;
}
function kurang(a, b) {
return a - b;
}
function kali(a, b) {
return a * b;
}
function bagi(a, b) {
if (b === 0) {
throw new Error('Tidak bisa membagi dengan nol!');
}
return a / b;
}
function faktorial(n) {
if (n < 0) throw new Error('Faktorial tidak untuk bilangan negatif');
if (n === 0 || n === 1) return 1;
return n * faktorial(n - 1);
}
module.exports = { tambah, kurang, kali, bagi, faktorial };
// math.test.js — File test
const { tambah, kurang, kali, bagi, faktorial } = require('./math');
// describe = mengelompokkan test yang berhubungan
describe('Fungsi Kalkulator', () => {
// test() atau it() — keduanya sama
test('menjumlahkan 2 bilangan positif', () => {
expect(tambah(2, 3)).toBe(5);
expect(tambah(10, 20)).toBe(30);
expect(tambah(0, 0)).toBe(0);
});
test('menjumlahkan bilangan negatif', () => {
expect(tambah(-2, -3)).toBe(-5);
expect(tambah(-5, 5)).toBe(0);
});
test('mengurangkan 2 bilangan', () => {
expect(kurang(10, 3)).toBe(7);
expect(kurang(3, 10)).toBe(-7);
});
test('mengalikan 2 bilangan', () => {
expect(kali(4, 5)).toBe(20);
expect(kali(0, 100)).toBe(0);
expect(kali(-3, 4)).toBe(-12);
});
test('membagi 2 bilangan', () => {
expect(bagi(10, 2)).toBe(5);
expect(bagi(7, 2)).toBe(3.5);
});
test('membagi dengan nol melempar error', () => {
expect(() => bagi(10, 0)).toThrow('Tidak bisa membagi dengan nol!');
});
});
describe('Fungsi Faktorial', () => {
test('faktorial dari 0 adalah 1', () => {
expect(faktorial(0)).toBe(1);
});
test('faktorial dari 5 adalah 120', () => {
expect(faktorial(5)).toBe(120);
});
test('faktorial negatif melempar error', () => {
expect(() => faktorial(-1)).toThrow();
});
});
Lifecycle Hooks
// Jest menyediakan lifecycle hooks untuk setup dan teardown
describe('Database Tests', () => {
let db;
// Berjalan SEBELUM semua test dalam describe
beforeAll(() => {
db = connectToTestDatabase();
console.log('Database connected');
});
// Berjalan SEBELUM setiap test
beforeEach(() => {
db.clear(); // Bersihkan data sebelum setiap test
console.log('Database cleared');
});
// Berjalan SETELAH setiap test
afterEach(() => {
console.log('Test selesai');
});
// Berjalan SETELAH semua test dalam describe
afterAll(() => {
db.disconnect();
console.log('Database disconnected');
});
test('menambahkan data', () => {
db.insert({ nama: 'Budi' });
expect(db.count()).toBe(1);
});
test('menghapus data', () => {
db.insert({ nama: 'Budi' });
db.delete('Budi');
expect(db.count()).toBe(0);
});
// Kedua test di atas punya database bersih
// berkat beforeEach yang membersihkan data
});
3. Matchers: Berbagai Cara Assertion
Matchers adalah method yang digunakan dalam expect() untuk memverifikasi berbagai jenis hasil. Jest menyediakan banyak matcher built-in yang sangat ekspresif.
Equality Matchers
// === (strict equality)
test('toBe: strict equality', () => {
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');
expect(true).toBe(true);
});
// Deep equality untuk object/array
test('toEqual: deep equality', () => {
expect({ nama: 'Budi', umur: 25 }).toEqual({ nama: 'Budi', umur: 25 });
expect([1, 2, 3]).toEqual([1, 2, 3]);
// toEqual cocok untuk object — tidak peduli referensi
const a = { x: 1 };
const b = { x: 1 };
expect(a).toEqual(b); // ✅ Pass (sama isi)
expect(a).toBe(b); // ❌ Fail (beda referensi)
});
// Negasi
test('not: membalik assertion', () => {
expect(2 + 2).not.toBe(5);
expect('hello').not.toBe('world');
expect([1, 2]).not.toEqual([3, 4]);
});
Truthiness Matchers
// Null, Undefined, Truthiness
test('truthiness matchers', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(7).toBeDefined(); // Bukan undefined
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
// Contoh penggunaan praktis
const user = { nama: 'Budi' };
expect(user.nama).toBeTruthy();
expect(user.email).toBeUndefined();
});
Number Matchers
// Perbandingan angka
test('number matchers', () => {
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);
// Floating point (angka desimal)
expect(0.1 + 0.2).toBeCloseTo(0.3); // Hindari masalah floating point
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // Presisi 5 digit
});
// Fungsi yang menghitung harga diskon
function hitungDiskon(harga, persen) {
return harga - (harga * persen / 100);
}
test('hitung diskon benar', () => {
expect(hitungDiskon(100000, 10)).toBeCloseTo(90000);
expect(hitungDiskon(50000, 20)).toBeCloseTo(40000);
expect(hitungDiskon(200000, 0)).toBeCloseTo(200000);
});
String & Array Matchers
// String matchers
test('string matchers', () => {
expect('Hello World').toMatch(/World/); // Regex match
expect('Hello World').toMatch('World'); // Substring match
expect('Budi Santoso').toMatch(/^Budi/); // Regex dengan anchor
expect('email@test.com').toMatch(/@/);
});
// Array & Iterable matchers
test('array matchers', () => {
const buah = ['apel', 'jeruk', 'mangga', 'pisang'];
expect(buah).toContain('mangga');
expect(buah).toHaveLength(4);
expect(buah).not.toContain('durian');
// Array of objects
const produk = [
{ id: 1, nama: 'Laptop' },
{ id: 2, nama: 'Mouse' },
];
expect(produk).toContainEqual({ id: 1, nama: 'Laptop' });
});
Error Matchers
// Menguji error/exception
function validateAge(umur) {
if (typeof umur !== 'number') throw new Error('Umur harus angka');
if (umur < 0) throw new Error('Umur tidak boleh negatif');
if (umur > 150) throw new Error('Umur tidak valid');
return true;
}
test('validasi umur berhasil', () => {
expect(validateAge(25)).toBe(true);
expect(validateAge(0)).toBe(true);
});
test('validasi umur gagal untuk input non-angka', () => {
expect(() => validateAge('abc')).toThrow('Umur harus angka');
});
test('validasi umur gagal untuk umur negatif', () => {
expect(() => validateAge(-5)).toThrow('Umur tidak boleh negatif');
});
test('validasi umur gagal untuk umur terlalu besar', () => {
expect(() => validateAge(200)).toThrow(/tidak valid/);
});
4. Mocking: Mengisolasi Dependensi
Mocking adalah teknik mengganti dependensi eksternal (API, database, layanan lain) dengan "tiruan" yang perilakunya bisa dikontrol. Ini memastikan kita menguji hanya kode yang sedang dibahas, bukan dependensi luar.
Mock Function (jest.fn)
// jest.fn() membuat fungsi tiruan yang melacak semua pemanggilan
test('mock function dasar', () => {
const mockCallback = jest.fn();
// Gunakan mock sebagai callback
[1, 2, 3].forEach(mockCallback);
// Verifikasi mock dipanggil 3 kali
expect(mockCallback).toHaveBeenCalledTimes(3);
// Verifikasi argumen setiap pemanggilan
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenCalledWith(2);
expect(mockCallback).toHaveBeenCalledWith(3);
// Akses detail pemanggilan
expect(mockCallback.mock.calls.length).toBe(3);
expect(mockCallback.mock.calls[0]).toEqual([1]); // Panggilan pertama
expect(mockCallback.mock.calls[1]).toEqual([2]); // Panggilan kedua
});
// Mock dengan return value
test('mock dengan return value', () => {
const mockFetch = jest.fn();
// Set return value untuk setiap pemanggilan
mockFetch
.mockReturnValueOnce({ status: 200, data: 'OK' })
.mockReturnValueOnce({ status: 404, data: 'Not Found' });
expect(mockFetch()).toEqual({ status: 200, data: 'OK' });
expect(mockFetch()).toEqual({ status: 404, data: 'Not Found' });
});
// Mock dengan implementasi custom
test('mock dengan implementasi', () => {
const mockTambah = jest.fn((a, b) => a * 2); // Salah sengaja!
expect(mockTambah(3, 4)).toBe(6); // 3 * 2 = 6 (bukan 7)
});
Mocking Module
// Misal ada modul API:
// api.js
// export async function getUsers() {
// const res = await fetch('https://api.example.com/users');
// return res.json();
// }
// api.test.js — Mock seluruh modul api
jest.mock('./api'); // Auto-mock semua export dari api.js
const { getUsers } = require('./api');
test('menggunakan mock getUsers', async () => {
// Atur return value mock
getUsers.mockResolvedValue([
{ id: 1, nama: 'Budi' },
{ id: 2, nama: 'Sari' },
]);
const users = await getUsers();
expect(users).toHaveLength(2);
expect(users[0].nama).toBe('Budi');
expect(getUsers).toHaveBeenCalledTimes(1);
});
// Mocking dengan partial mock
jest.mock('./api', () => ({
...jest.requireActual('./api'), // Pertahankan implementasi asli
getUsers: jest.fn(), // Override hanya getUsers
}));
Mocking HTTP Request (fetch)
// Mock global fetch
global.fetch = jest.fn();
// Fungsi yang akan di-test
async function ambilProduk(id) {
const response = await fetch(`/api/produk/${id}`);
if (!response.ok) {
throw new Error('Produk tidak ditemukan');
}
return response.json();
}
// Test suite
describe('ambilProduk', () => {
beforeEach(() => {
fetch.mockClear(); // Reset mock sebelum setiap test
});
test('berhasil mengambil produk', async () => {
// Setup mock response
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, nama: 'Laptop', harga: 12000000 }),
});
const produk = await ambilProduk(1);
expect(produk.nama).toBe('Laptop');
expect(produk.harga).toBe(12000000);
expect(fetch).toHaveBeenCalledWith('/api/produk/1');
});
test('gagal jika produk tidak ditemukan', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
await expect(ambilProduk(999)).rejects.toThrow('Produk tidak ditemukan');
});
});
Mock digunakan untuk mengisolasi kode yang sedang di-test dari dependensi eksternal. Aturan praktis: mock hal-hal yang tidak Anda kontrol (API eksternal, database, filesystem, waktu). Jangan mock hal-hal yang Anda kontrol (fungsi utilitas internal, pure functions) — test mereka secara langsung.
5. Async Testing
Banyak kode JavaScript modern bersifat asinkron — Promises, async/await, callbacks. Jest menyediakan cara yang jelas untuk menguji kode async.
Testing dengan Promises
// Fungsi async yang akan di-test
async function fetchUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
const user = await response.json();
return user;
}
async function fetchUserPosts(userId) {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!response.ok) throw new Error('Gagal mengambil posts');
return response.json();
}
// Cara 1: Menggunakan async/await (DIREKOMENDASIKAN)
test('fetchUser mengembalikan data user', async () => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ id: 1, nama: 'Budi' }),
});
const user = await fetchUser(1);
expect(user.nama).toBe('Budi');
expect(user.id).toBe(1);
});
// Cara 2: Menggunakan .resolves / .rejects
test('fetchUser mengembalikan data (dengan resolves)', async () => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ id: 1, nama: 'Sari' }),
});
await expect(fetchUser(1)).resolves.toEqual({ id: 1, nama: 'Sari' });
});
// Testing error async
test('fetchUserPosts melempar error saat gagal', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 500,
});
await expect(fetchUserPosts(1)).rejects.toThrow('Gagal mengambil posts');
});
// Cara 3: Menggunakan .then() (kurang direkomendasikan)
test('fetchUser dengan .then()', () => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ id: 1, nama: 'Andi' }),
});
return fetchUser(1).then(user => {
expect(user.nama).toBe('Andi');
});
});
Testing dengan Timer Mock
// Fungsi dengan setTimeout
function delayedGreeting(name, callback) {
setTimeout(() => {
callback(`Halo, ${name}!`);
}, 2000);
}
// Menggunakan fake timers
jest.useFakeTimers();
test('delayedGreeting memanggil callback setelah 2 detik', () => {
const callback = jest.fn();
delayedGreeting('Budi', callback);
// Belum dipanggil (belum 2 detik)
expect(callback).not.toHaveBeenCalled();
// Majukan waktu 2 detik
jest.advanceTimersByTime(2000);
// Sekarang seharusnya dipanggil
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('Halo, Budi!');
});
// Interval test
test('interval berjalan dengan benar', () => {
const callback = jest.fn();
setInterval(callback, 1000);
jest.advanceTimersByTime(3000); // Maju 3 detik
expect(callback).toHaveBeenCalledTimes(3);
});
// Kembalikan timer normal setelah test
afterEach(() => {
jest.useRealTimers();
});
6. Code Coverage
Code coverage mengukur berapa persen dari kode Anda yang dijalankan selama test. Ini membantu menemukan bagian kode yang belum ter-test.
Menjalankan Coverage
# Jalankan test dengan coverage
npm test -- --coverage
# Output:
# ----------|---------|----------|---------|---------|
# File | % Stmts | % Branch | % Funcs | % Lines |
# ----------|---------|----------|---------|---------|
# math.js | 95.45 | 83.33 | 100 | 95.45 |
# api.js | 72.73 | 50 | 66.67 | 72.73 |
# utils.js | 100 | 100 | 100 | 100 |
# ----------|---------|----------|---------|---------|
# Coverage metrics:
# - % Stmts = Statement coverage (baris kode yang dijalankan)
# - % Branch = Branch coverage (if/else yang ter-test semua)
# - % Funcs = Function coverage (fungsi yang dipanggil)
# - % Lines = Line coverage (baris yang ter-execution)
# Threshold di jest.config.js:
# coverageThreshold: {
# global: {
# branches: 80,
# functions: 80,
# lines: 80,
# statements: 80,
# }
# }
Coverage tinggi bagus, tapi bukan segalanya. Test yang tidak bermakna bisa mencapai 100% coverage tanpa benar-benar menguji logika. Fokus pada test yang bermakna — uji behavior, edge cases, dan skenario error — bukan sekadar mengejar angka coverage.
7. Pengenalan Test-Driven Development (TDD)
Test-Driven Development (TDD) adalah metodologi pengembangan di mana Anda menulis test TERLEBIH DAHULU, baru menulis kode yang memenuhi test tersebut. Siklus TDD dikenal sebagai Red → Green → Refactor.
Siklus TDD: Red → Green → Refactor
┌───────────────────────────────────────────────────────┐ │ SIKLUS TDD: RED → GREEN → REFACTOR │ │ │ │ ┌──────────────┐ │ │ │ 1. RED │ Tulis test yang GAGAL │ │ │ 🔴 FAIL │ (kode belum ada) │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 2. GREEN │ Tulis kode secukupnya │ │ │ ✅ PASS │ agar test LULUS │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 3. REFACTOR │ Perbaiki kode tanpa │ │ │ 🔄 IMPROVE │ mengubah behavior │ │ └──────┬───────┘ │ │ │ │ │ └──────────▶ Kembali ke RED │ │ │ │ Ulangi siklus ini untuk setiap fitur/tambah │ └───────────────────────────────────────────────────────┘
Contoh TDD: Membuat Shopping Cart
// === LANGKAH 1: RED — Tulis test dulu (belum ada kode) ===
// shopping-cart.test.js
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart();
});
test('keranjang baru kosong', () => {
expect(cart.getItems()).toEqual([]);
expect(cart.getTotal()).toBe(0);
});
test('menambahkan produk ke keranjang', () => {
cart.addItem({ id: 1, nama: 'Laptop', harga: 12000000 });
expect(cart.getItems()).toHaveLength(1);
expect(cart.getItems()[0].nama).toBe('Laptop');
});
test('menghitung total harga', () => {
cart.addItem({ id: 1, nama: 'Laptop', harga: 12000000 });
cart.addItem({ id: 2, nama: 'Mouse', harga: 250000 });
expect(cart.getTotal()).toBe(12250000);
});
test('menghapus produk dari keranjang', () => {
cart.addItem({ id: 1, nama: 'Laptop', harga: 12000000 });
cart.removeItem(1);
expect(cart.getItems()).toHaveLength(0);
});
test('menambah quantity produk yang sama', () => {
cart.addItem({ id: 1, nama: 'Laptop', harga: 12000000 });
cart.addItem({ id: 1, nama: 'Laptop', harga: 12000000 });
expect(cart.getItems()).toHaveLength(1);
expect(cart.getItems()[0].quantity).toBe(2);
expect(cart.getTotal()).toBe(24000000);
});
});
// === LANGKAH 2: GREEN — Tulis kode agar test lulus ===
// shopping-cart.js
class ShoppingCart {
constructor() {
this.items = [];
}
getItems() {
return this.items;
}
addItem(product) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
this.items.push({ ...product, quantity: 1 });
}
}
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.harga * item.quantity, 0);
}
}
// === LANGKAH 3: REFACTOR — Perbaiki kode ===
// (Tambah validasi, optimasi, clean code — test tetap pass!)
TDD memaksa Anda berpikir tentang behavior kode SEBELUM menulis kode. Hasilnya: kode yang lebih modular, lebih terdokumentasi, dan lebih mudah di-maintain. TDD juga mengurangi waktu debugging karena Anda punya test yang mendeteksi masalah sejak awal.
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang testing dengan Jest: