Kuasai pola pengujian Jest tingkat lanjut untuk membangun perangkat lunak yang andal. Jelajahi mocking, snapshot testing, custom matchers, dan lainnya.
Jest: Pola Pengujian Tingkat Lanjut untuk Perangkat Lunak yang Tangguh
Dalam lanskap pengembangan perangkat lunak yang serba cepat saat ini, memastikan keandalan dan stabilitas basis kode Anda adalah hal yang terpenting. Meskipun Jest telah menjadi standar de facto untuk pengujian JavaScript, melampaui pengujian unit dasar akan membuka tingkat kepercayaan baru pada aplikasi Anda. Postingan ini membahas pola pengujian Jest tingkat lanjut yang penting untuk membangun perangkat lunak yang tangguh, yang diperuntukkan bagi audiens pengembang global.
Mengapa Melampaui Pengujian Unit Dasar?
Pengujian unit dasar memverifikasi komponen individual secara terpisah. Namun, aplikasi di dunia nyata adalah sistem kompleks tempat komponen berinteraksi. Pola pengujian tingkat lanjut mengatasi kompleksitas ini dengan memungkinkan kita untuk:
- Menyimulasikan dependensi yang kompleks.
- Menangkap perubahan UI secara andal.
- Menulis pengujian yang lebih ekspresif dan mudah dikelola.
- Meningkatkan cakupan pengujian dan kepercayaan pada titik integrasi.
- Memfasilitasi alur kerja Test-Driven Development (TDD) dan Behavior-Driven Development (BDD).
Menguasai Mocking dan Spies
Mocking sangat penting untuk mengisolasi unit yang diuji dengan mengganti dependensinya dengan pengganti yang terkontrol. Jest menyediakan alat yang kuat untuk ini:
jest.fn()
: Fondasi dari Mock dan Spy
jest.fn()
membuat fungsi tiruan (mock function). Anda dapat melacak pemanggilan, argumen, dan nilai kembaliannya. Ini adalah blok bangunan untuk strategi mocking yang lebih canggih.
Contoh: Melacak Pemanggilan Fungsi
// component.js
export const fetchData = () => {
// Mensimulasikan panggilan API
return Promise.resolve({ data: 'some data' });
};
export const processData = async (fetcher) => {
const result = await fetcher();
return `Processed: ${result.data}`;
};
// component.test.js
import { processData } from './component';
test('should process data correctly', async () => {
const mockFetcher = jest.fn().mockResolvedValue({ data: 'mocked data' });
const result = await processData(mockFetcher);
expect(result).toBe('Processed: mocked data');
expect(mockFetcher).toHaveBeenCalledTimes(1);
expect(mockFetcher).toHaveBeenCalledWith();
});
jest.spyOn()
: Mengamati Tanpa Mengganti
jest.spyOn()
memungkinkan Anda untuk mengamati pemanggilan metode pada objek yang ada tanpa harus mengganti implementasinya. Anda juga dapat meniru implementasinya jika diperlukan.
Contoh: Memata-matai Metode Modul
// logger.js
export const logInfo = (message) => {
console.log(`INFO: ${message}`);
};
// service.js
import { logInfo } from './logger';
export const performTask = (taskName) => {
logInfo(`Starting task: ${taskName}`);
// ... logika tugas ...
logInfo(`Task ${taskName} completed.`);
};
// service.test.js
import { performTask } from './service';
import * as logger from './logger';
test('should log task start and completion', () => {
const logSpy = jest.spyOn(logger, 'logInfo');
performTask('backup');
expect(logSpy).toHaveBeenCalledTimes(2);
expect(logSpy).toHaveBeenCalledWith('Starting task: backup');
expect(logSpy).toHaveBeenCalledWith('Task backup completed.');
logSpy.mockRestore(); // Penting untuk mengembalikan implementasi asli
});
Mocking Impor Modul
Kemampuan mocking modul Jest sangat luas. Anda dapat meniru seluruh modul atau ekspor tertentu.
Contoh: Mocking Klien API Eksternal
// api.js
import axios from 'axios';
export const getUser = async (userId) => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
};
// user-service.js
import { getUser } from './api';
export const getUserFullName = async (userId) => {
const user = await getUser(userId);
return `${user.firstName} ${user.lastName}`;
};
// user-service.test.js
import { getUserFullName } from './user-service';
import * as api from './api';
// Mock seluruh modul api
jest.mock('./api');
test('should get full name using mocked API', async () => {
// Mock fungsi spesifik dari modul yang di-mock
api.getUser.mockResolvedValue({ id: 1, firstName: 'Ada', lastName: 'Lovelace' });
const fullName = await getUserFullName(1);
expect(fullName).toBe('Ada Lovelace');
expect(api.getUser).toHaveBeenCalledTimes(1);
expect(api.getUser).toHaveBeenCalledWith(1);
});
Mocking Otomatis vs. Mocking Manual
Jest secara otomatis meniru (mock) modul Node.js. Untuk modul ES atau modul kustom, Anda mungkin memerlukan jest.mock()
. Untuk kontrol lebih, Anda dapat membuat direktori __mocks__
.
Implementasi Mock
Anda dapat menyediakan implementasi kustom untuk mock Anda.
Contoh: Mocking dengan Implementasi Kustom
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// calculator.js
import { add, subtract } from './math';
export const calculate = (operation, a, b) => {
if (operation === 'add') {
return add(a, b);
} else if (operation === 'subtract') {
return subtract(a, b);
}
return null;
};
// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';
// Mock seluruh modul math
jest.mock('./math');
test('should perform addition using mocked math add', () => {
// Sediakan implementasi mock untuk fungsi 'add'
math.add.mockImplementation((a, b) => a + b + 10); // Tambahkan 10 ke hasilnya
math.subtract.mockReturnValue(5); // Mock juga fungsi subtract
const result = calculate('add', 5, 3);
expect(math.add).toHaveBeenCalledWith(5, 3);
expect(result).toBe(18); // 5 + 3 + 10
const subResult = calculate('subtract', 10, 2);
expect(math.subtract).toHaveBeenCalledWith(10, 2);
expect(subResult).toBe(5);
});
Pengujian Snapshot: Menjaga UI dan Konfigurasi
Pengujian snapshot adalah fitur canggih untuk menangkap output dari komponen atau konfigurasi Anda. Fitur ini sangat berguna untuk pengujian UI atau memverifikasi struktur data yang kompleks.
Cara Kerja Pengujian Snapshot
Pertama kali pengujian snapshot dijalankan, Jest membuat file .snap
yang berisi representasi serial dari nilai yang diuji. Pada proses selanjutnya, Jest membandingkan output saat ini dengan snapshot yang tersimpan. Jika berbeda, pengujian akan gagal, memberitahu Anda tentang perubahan yang tidak diinginkan. Ini sangat berharga untuk mendeteksi regresi pada komponen UI di berbagai wilayah atau lokal.
Contoh: Membuat Snapshot Komponen React
Anggap Anda memiliki komponen React:
// UserProfile.js
import React from 'react';
const UserProfile = ({ name, email, isActive }) => (
<div>
<h2>{name}</h2>
<p><strong>Email:</strong> {email}</p>
<p><strong>Status:</strong> {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
export default UserProfile;
// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // Untuk snapshot komponen React
import UserProfile from './UserProfile';
test('renders UserProfile correctly', () => {
const user = {
name: 'Jane Doe',
email: 'jane.doe@example.com',
isActive: true,
};
const component = renderer.create(
<UserProfile {...user} />
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders inactive UserProfile correctly', () => {
const user = {
name: 'John Smith',
email: 'john.smith@example.com',
isActive: false,
};
const component = renderer.create(
<UserProfile {...user} />
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot('inactive user profile'); // Snapshot bernama
});
Setelah menjalankan pengujian, Jest akan membuat file UserProfile.test.js.snap
. Saat Anda memperbarui komponen, Anda perlu meninjau perubahan dan berpotensi memperbarui snapshot dengan menjalankan Jest dengan flag --updateSnapshot
atau -u
.
Praktik Terbaik untuk Pengujian Snapshot
- Gunakan untuk komponen UI dan file konfigurasi: Ideal untuk memastikan elemen UI dirender seperti yang diharapkan dan konfigurasi tidak berubah secara tidak sengaja.
- Tinjau snapshot dengan cermat: Jangan secara membabi buta menerima pembaruan snapshot. Selalu tinjau apa yang telah berubah untuk memastikan modifikasi tersebut disengaja.
- Hindari snapshot untuk data yang sering berubah: Jika data berubah dengan cepat, snapshot bisa menjadi rapuh dan menyebabkan terlalu banyak gangguan.
- Gunakan snapshot bernama: Untuk menguji beberapa status komponen, snapshot bernama memberikan kejelasan yang lebih baik.
Custom Matcher: Meningkatkan Keterbacaan Tes
Matcher bawaan Jest sangat banyak, tetapi terkadang Anda perlu menegaskan kondisi spesifik yang tidak tercakup. Custom matcher memungkinkan Anda membuat logika asersi sendiri, membuat pengujian Anda lebih ekspresif dan mudah dibaca.
Membuat Custom Matcher
Anda dapat memperluas objek expect
Jest dengan matcher Anda sendiri.
Contoh: Memeriksa Format Email yang Valid
Di file penyiapan Jest Anda (misalnya, jest.setup.js
, dikonfigurasi di jest.config.js
):
// jest.setup.js
expect.extend({
toBeValidEmail(received) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
if (pass) {
return {
message: () => `mengharapkan ${received} bukan email yang valid`,
pass: true,
};
} else {
return {
message: () => `mengharapkan ${received} adalah email yang valid`,
pass: false,
};
}
},
});
// Di jest.config.js Anda
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };
Di file pengujian Anda:
// validation.test.js
test('should validate email formats', () => {
expect('test@example.com').toBeValidEmail();
expect('invalid-email').not.toBeValidEmail();
expect('another.test@sub.domain.co.uk').toBeValidEmail();
});
Manfaat Custom Matcher
- Keterbacaan yang Ditingkatkan: Pengujian menjadi lebih deklaratif, menyatakan *apa* yang diuji daripada *bagaimana*.
- Penggunaan Ulang Kode: Hindari pengulangan logika asersi yang kompleks di beberapa pengujian.
- Asersi Spesifik Domain: Sesuaikan asersi dengan persyaratan domain spesifik aplikasi Anda.
Menguji Operasi Asinkron
JavaScript sangat bergantung pada operasi asinkron. Jest menyediakan dukungan yang sangat baik untuk menguji promise dan async/await.
Menggunakan async/await
Ini adalah cara modern dan paling mudah dibaca untuk menguji kode asinkron.
Contoh: Menguji Fungsi Asinkron
// dataService.js
export const fetchUserData = async (userId) => {
// Mensimulasikan pengambilan data setelah jeda
await new Promise(resolve => setTimeout(resolve, 50));
if (userId === 1) {
return { id: 1, name: 'Alice' };
} else {
throw new Error('Pengguna tidak ditemukan');
}
};
// dataService.test.js
import { fetchUserData } from './dataService';
test('mengambil data pengguna dengan benar', async () => {
const user = await fetchUserData(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
test('melemparkan galat untuk pengguna yang tidak ada', async () => {
await expect(fetchUserData(2)).rejects.toThrow('Pengguna tidak ditemukan');
});
Menggunakan .resolves
dan .rejects
Matcher ini menyederhanakan pengujian resolusi dan penolakan promise.
Contoh: Menggunakan .resolves/.rejects
// dataService.test.js (lanjutan)
test('mengambil data pengguna dengan .resolves', () => {
return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});
test('melemparkan galat untuk pengguna yang tidak ada dengan .rejects', () => {
return expect(fetchUserData(2)).rejects.toThrow('Pengguna tidak ditemukan');
});
Menangani Timer
Untuk fungsi yang menggunakan setTimeout
atau setInterval
, Jest menyediakan kontrol timer.
Contoh: Mengontrol Timer
// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
setTimeout(() => {
callback(`Hello, ${name}!`);
}, 1000);
};
// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';
jest.useFakeTimers(); // Aktifkan timer palsu
test('menyapa setelah jeda', () => {
const mockCallback = jest.fn();
greetAfterDelay('World', mockCallback);
// Majukan timer sebesar 1000ms
jest.advanceTimersByTime(1000);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});
// Kembalikan timer asli jika diperlukan di tempat lain
jest.useRealTimers();
Organisasi dan Struktur Tes
Seiring bertambahnya rangkaian pengujian Anda, organisasi menjadi sangat penting untuk kemudahan pemeliharaan.
Blok Describe dan It
Gunakan describe
untuk mengelompokkan pengujian terkait dan it
(atau test
) untuk kasus pengujian individual. Struktur ini mencerminkan modularitas aplikasi.
Contoh: Pengujian Terstruktur
describe('User Authentication Service', () => {
let authService;
beforeEach(() => {
// Siapkan mock atau instance layanan sebelum setiap tes
authService = require('./authService');
jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
});
afterEach(() => {
// Bersihkan mock
jest.restoreAllMocks();
});
describe('login functionality', () => {
it('should successfully log in a user with valid credentials', async () => {
const result = await authService.login('user@example.com', 'password123');
expect(result.token).toBeDefined();
// ... asersi lainnya ...
});
it('should fail login with invalid credentials', async () => {
jest.spyOn(authService, 'login').mockRejectedValue(new Error('Invalid credentials'));
await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Invalid credentials');
});
});
describe('logout functionality', () => {
it('should clear user session', async () => {
// Uji logika logout...
});
});
});
Hook Setup dan Teardown
beforeAll
: Berjalan sekali sebelum semua tes dalam blokdescribe
.afterAll
: Berjalan sekali setelah semua tes dalam blokdescribe
.beforeEach
: Berjalan sebelum setiap tes dalam blokdescribe
.afterEach
: Berjalan setelah setiap tes dalam blokdescribe
.
Hook ini penting untuk menyiapkan data mock, koneksi basis data, atau membersihkan sumber daya di antara pengujian.
Pengujian untuk Audiens Global
Saat mengembangkan aplikasi untuk audiens global, pertimbangan pengujian meluas:
Internasionalisasi (i18n) dan Lokalisasi (l10n)
Pastikan UI dan pesan Anda beradaptasi dengan benar ke berbagai bahasa dan format regional.
- Snapshotting UI yang dilokalkan: Uji bahwa versi bahasa yang berbeda dari UI Anda dirender dengan benar menggunakan pengujian snapshot.
- Mocking data lokal: Mock pustaka seperti
react-intl
ataui18next
untuk menguji perilaku komponen dengan pesan lokal yang berbeda. - Pemformatan Tanggal, Waktu, dan Mata Uang: Uji bahwa ini ditangani dengan benar menggunakan custom matcher atau dengan mocking pustaka internasionalisasi. Misalnya, memverifikasi bahwa tanggal yang diformat untuk Jerman (DD.MM.YYYY) tampil berbeda dari untuk AS (MM/DD/YYYY).
Contoh: Menguji pemformatan tanggal yang dilokalkan
// dateUtils.js
export const formatLocalizedDate = (date, locale) => {
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(date);
};
// dateUtils.test.js
import { formatLocalizedDate } from './dateUtils';
test('memformat tanggal dengan benar untuk lokal AS', () => {
const date = new Date(2023, 10, 15); // 15 November 2023
expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});
test('memformat tanggal dengan benar untuk lokal Jerman', () => {
const date = new Date(2023, 10, 15);
expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});
Kewaspadaan Zona Waktu
Uji bagaimana aplikasi Anda menangani zona waktu yang berbeda, terutama untuk fitur seperti penjadwalan atau pembaruan waktu nyata. Melakukan mock pada jam sistem atau menggunakan pustaka yang mengabstraksikan zona waktu bisa bermanfaat.
Nuansa Budaya dalam Data
Pertimbangkan bagaimana angka, mata uang, dan representasi data lainnya mungkin dirasakan atau diharapkan secara berbeda di berbagai budaya. Custom matcher dapat sangat berguna di sini.
Teknik dan Strategi Tingkat Lanjut
Test-Driven Development (TDD) dan Behavior-Driven Development (BDD)
Jest selaras dengan metodologi TDD (Red-Green-Refactor) dan BDD (Given-When-Then). Tulis pengujian yang menggambarkan perilaku yang diinginkan sebelum menulis kode implementasi. Ini memastikan bahwa kode ditulis dengan mempertimbangkan kemampuan pengujian sejak awal.
Pengujian Integrasi dengan Jest
Meskipun Jest unggul dalam pengujian unit, ia juga dapat digunakan untuk pengujian integrasi. Melakukan mock lebih sedikit dependensi atau menggunakan alat seperti opsi runInBand
Jest dapat membantu.
Contoh: Menguji Interaksi API (disederhanakan)
// apiService.js
import axios from 'axios';
const API_BASE_URL = 'https://api.example.com';
export const createProduct = async (productData) => {
const response = await axios.post(`${API_BASE_URL}/products`, productData);
return response.data;
};
// apiService.test.js (Uji Integrasi)
import axios from 'axios';
import { createProduct } from './apiService';
// Mock axios untuk pengujian integrasi guna mengontrol lapisan jaringan
jest.mock('axios');
test('membuat produk melalui API', async () => {
const mockProduct = { id: 1, name: 'Gadget' };
const responseData = { success: true, product: mockProduct };
axios.post.mockResolvedValue({
data: responseData,
status: 201,
headers: { 'content-type': 'application/json' },
});
const newProductData = { name: 'Gadget', price: 99.99 };
const result = await createProduct(newProductData);
expect(axios.post).toHaveBeenCalledWith(`${process.env.API_BASE_URL || 'https://api.example.com'}/products`, newProductData);
expect(result).toEqual(responseData);
});
Paralelisme dan Konfigurasi
Jest dapat menjalankan pengujian secara paralel untuk mempercepat eksekusi. Konfigurasikan ini di jest.config.js
Anda. Misalnya, mengatur maxWorkers
mengontrol jumlah proses paralel.
Laporan Cakupan
Gunakan pelaporan cakupan bawaan Jest untuk mengidentifikasi bagian-bagian dari basis kode Anda yang tidak diuji. Jalankan pengujian dengan --coverage
untuk menghasilkan laporan terperinci.
jest --coverage
Meninjau laporan cakupan membantu memastikan bahwa pola pengujian tingkat lanjut Anda secara efektif mencakup logika kritis, termasuk jalur kode internasionalisasi dan lokalisasi.
Kesimpulan
Menguasai pola pengujian Jest tingkat lanjut adalah langkah signifikan menuju pembangunan perangkat lunak yang andal, mudah dikelola, dan berkualitas tinggi untuk audiens global. Dengan memanfaatkan mocking, pengujian snapshot, custom matcher, dan teknik pengujian asinkron secara efektif, Anda dapat meningkatkan ketangguhan rangkaian pengujian Anda dan mendapatkan kepercayaan yang lebih besar terhadap perilaku aplikasi Anda di berbagai skenario dan wilayah. Menerapkan pola-pola ini memberdayakan tim pengembangan di seluruh dunia untuk memberikan pengalaman pengguna yang luar biasa.
Mulai gabungkan teknik-teknik canggih ini ke dalam alur kerja Anda hari ini untuk meningkatkan praktik pengujian JavaScript Anda.