Unit Testing JavaScript dengan Jest: Panduan Lengkap
Testing adalah bagian penting dari software development yang sering diabaikan. Dengan unit testing, kamu bisa memastikan kode bekerja sesuai ekspektasi dan mencegah bug sebelum masuk ke production.
Mengapa Unit Testing?
- Catch bugs early: Temukan bug sebelum deploy
- Refactoring dengan percaya diri: Ubah kode tanpa takut merusak fitur
- Documentation: Test sebagai dokumentasi behavior kode
- Better design: Kode yang testable biasanya lebih modular
Setup Jest
Instalasi
# npm
npm install --save-dev jest
# yarn
yarn add --dev jest
Konfigurasi package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Konfigurasi Jest (opsional)
Buat file jest.config.js:
module.exports = {
testEnvironment: 'node',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
collectCoverageFrom: ['src/**/*.js', '!src/**/*.test.js'],
};
Menulis Test Pertama
Struktur Dasar
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// math.test.js
const { add, subtract } = require('./math');
describe('Math functions', () => {
describe('add', () => {
test('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
test('should add zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('subtract', () => {
test('should subtract two numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
});
});
Menjalankan Test
npm test
Matchers
Jest menyediakan berbagai matchers untuk assertion:
Equality
test('equality matchers', () => {
// Exact equality
expect(2 + 2).toBe(4);
// Object equality (deep)
expect({ name: 'John' }).toEqual({ name: 'John' });
// Not equal
expect(2 + 2).not.toBe(5);
});
Truthiness
test('truthiness matchers', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();
});
Numbers
test('number matchers', () => {
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);
// Floating point
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
Strings
test('string matchers', () => {
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');
expect('Hello').toHaveLength(5);
});
Arrays
test('array matchers', () => {
const fruits = ['apple', 'banana', 'orange'];
expect(fruits).toContain('banana');
expect(fruits).toHaveLength(3);
expect(fruits).toEqual(expect.arrayContaining(['apple', 'orange']));
});
Objects
test('object matchers', () => {
const user = {
name: 'John',
email: 'john@example.com',
age: 25,
};
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('name', 'John');
expect(user).toMatchObject({ name: 'John', email: 'john@example.com' });
});
Exceptions
function throwError() {
throw new Error('Something went wrong');
}
test('exception matchers', () => {
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow('Something went wrong');
expect(() => throwError()).toThrow(Error);
});
Testing Async Code
Callbacks
function fetchData(callback) {
setTimeout(() => {
callback('data');
}, 100);
}
test('callback test', (done) => {
fetchData((data) => {
expect(data).toBe('data');
done();
});
});
Promises
function fetchUser() {
return Promise.resolve({ name: 'John' });
}
// Menggunakan return
test('promise test with return', () => {
return fetchUser().then((user) => {
expect(user.name).toBe('John');
});
});
// Menggunakan resolves
test('promise test with resolves', () => {
return expect(fetchUser()).resolves.toEqual({ name: 'John' });
});
Async/Await
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
test('async/await test', async () => {
const user = await getUser(1);
expect(user.name).toBe('John');
});
test('async error handling', async () => {
await expect(getUser(-1)).rejects.toThrow('User not found');
});
Mocking
Mock Functions
test('mock function', () => {
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
});
Mock Return Values
test('mock return values', () => {
const mockFn = jest.fn();
mockFn.mockReturnValueOnce(10).mockReturnValueOnce(20).mockReturnValue(30);
expect(mockFn()).toBe(10);
expect(mockFn()).toBe(20);
expect(mockFn()).toBe(30);
expect(mockFn()).toBe(30);
});
Mock Implementations
test('mock implementation', () => {
const mockFn = jest.fn((x) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn(3)).toBe(6);
});
Mocking Modules
// userService.js
const axios = require('axios');
async function getUser(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
module.exports = { getUser };
// userService.test.js
const axios = require('axios');
const { getUser } = require('./userService');
jest.mock('axios');
test('should fetch user', async () => {
const mockUser = { id: 1, name: 'John' };
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUser(1);
expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
Spying
const calculator = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
test('spy on method', () => {
const spy = jest.spyOn(calculator, 'add');
calculator.add(2, 3);
expect(spy).toHaveBeenCalledWith(2, 3);
spy.mockRestore();
});
Setup dan Teardown
describe('Database tests', () => {
// Jalankan sekali sebelum semua test
beforeAll(async () => {
await db.connect();
});
// Jalankan sekali setelah semua test
afterAll(async () => {
await db.disconnect();
});
// Jalankan sebelum setiap test
beforeEach(async () => {
await db.clear();
});
// Jalankan setelah setiap test
afterEach(() => {
jest.clearAllMocks();
});
test('should create user', async () => {
// test code
});
});
Testing Best Practices
1. Arrange-Act-Assert (AAA)
test('should calculate total price', () => {
// Arrange
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 3 },
];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(350);
});
2. Test One Thing at a Time
// ❌ Bad - testing multiple things
test('user operations', () => {
const user = createUser('John');
expect(user.name).toBe('John');
updateUser(user, { age: 25 });
expect(user.age).toBe(25);
deleteUser(user);
expect(user.deleted).toBe(true);
});
// ✅ Good - separate tests
test('should create user with name', () => {
const user = createUser('John');
expect(user.name).toBe('John');
});
test('should update user age', () => {
const user = createUser('John');
updateUser(user, { age: 25 });
expect(user.age).toBe(25);
});
3. Use Descriptive Test Names
// ❌ Bad
test('test1', () => {});
// ✅ Good
test('should return empty array when no items match filter', () => {});
4. Don’t Test Implementation Details
// ❌ Bad - testing internal state
test('should set internal flag', () => {
const cart = new Cart();
cart.addItem({ id: 1 });
expect(cart._items.length).toBe(1); // internal
});
// ✅ Good - testing behavior
test('should have one item after adding', () => {
const cart = new Cart();
cart.addItem({ id: 1 });
expect(cart.getItemCount()).toBe(1);
});
Code Coverage
Jalankan test dengan coverage:
npm test -- --coverage
Output akan menunjukkan:
- Statements: Persentase statement yang dieksekusi
- Branches: Persentase branch (if/else) yang dieksekusi
- Functions: Persentase function yang dipanggil
- Lines: Persentase baris yang dieksekusi
Kesimpulan
Unit testing dengan Jest membantu kamu:
- Menulis kode yang lebih reliable
- Refactor dengan percaya diri
- Dokumentasi behavior kode
- Catch bugs lebih awal
Mulai dengan test sederhana, lalu tingkatkan coverage secara bertahap. Ingat, test yang baik adalah test yang mudah dibaca dan di-maintain.
Referensi:
Komentar
Memuat komentar...
Tulis Komentar