Javascript Testing Unit Integration Mocking Async Tests
Definisi
Testing di JavaScript adalah cara untuk memverifikasi bahwa kode berjalan sesuai perilaku yang diharapkan, bukan hanya sekadar lolos saat dijalankan sekali. Di kerja harian, testing dipakai untuk menangkap regresi, menjaga kontrak perilaku, dan memberi sinyal cepat saat refactor merusak hal yang tidak kelihatan dari hasil akhir saja.
Topik ini biasanya mencakup empat hal utama:
- unit testing: menguji fungsi atau unit logika kecil secara terisolasi
- integration testing: menguji kerja sama antarbagian, misalnya UI + service + fetch + parser
- mocking: mengganti dependency yang mahal, lambat, atau tidak stabil dengan versi kontrol
- test structure: cara menata test supaya mudah dibaca, dirawat, dan di-debug
Kalau kode mulai tumbuh, testing bukan lagi opsi tambahan. Testing jadi alat untuk menjaga perubahan tetap aman, terutama saat ada beberapa layer yang saling memengaruhi.
Kapan testing dipakai
Testing paling berguna ketika kamu ingin jawaban cepat untuk pertanyaan seperti:
- apakah fungsi ini masih mengembalikan hasil yang benar setelah refactor?
- apakah perubahan state ini masih memicu output yang tepat?
- apakah request ke backend masih ditangani dengan benar?
- apakah error branch, retry, atau fallback masih bekerja?
- apakah bug yang kemarin sudah benar-benar tertutup?
Kalau diringkas, testing dipakai saat:
- logika mulai punya cabang perilaku
- ada risiko regresi setelah perubahan
- dependency eksternal tidak bisa dipercaya 100% saat test
- behavior penting lebih mahal dicek manual setiap kali
- team perlu spesifikasi perilaku yang bisa dibaca ulang
Kapan testing paling terasa nilainya
- fungsi transformasi data
- validasi input
- formatter dan parser
- komponen UI yang punya state dan event
- flow async: fetch, submit, loading, error, retry
- boundary integrasi: adapter API, repository, service layer
Testing biasanya paling terasa manfaatnya saat bug muncul berulang. Begitu bug bisa ditulis sebagai test, dia berhenti jadi cerita lisan dan berubah jadi spesifikasi yang bisa dijalankan ulang.
Test pyramid
Secara praktis, komposisi test yang sehat sering digambarkan sebagai piramida: banyak unit test, lebih sedikit integration test, dan lebih sedikit lagi end-to-end test.
Kenapa piramida ini masuk akal
- unit test cepat dijalankan dan mudah dipersempit saat gagal
- integration test berguna untuk memastikan boundary antar modul benar
- E2E test mahal, lebih lambat, dan lebih rentan flake
- makin dekat ke bawah, feedback makin cepat
Bukan berarti integration test itu buruk atau unit test selalu cukup. Yang sehat adalah proporsinya: logika inti banyak diuji di level kecil, sedangkan alur penting di level integrasi mendapat cakupan yang cukup.
Unit testing
Unit testing menguji satu unit perilaku kecil dengan dependency seminimal mungkin. Di JavaScript, unit test sering dipakai untuk fungsi pure, helper, validator, reducer, formatter, atau logic kecil yang hasilnya deterministik.
Ciri unit test
- fokus ke satu perilaku kecil
- cepat
- input dan output jelas
- dependency luar diminimalkan atau dimock
- mudah dibaca hanya dari nama test dan assert-nya
Contoh unit test
// formatPrice.js
export function formatPrice(amount) {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
}).format(amount);
}import { formatPrice } from './formatPrice.js';
describe('formatPrice', () => {
it('memformat angka ke rupiah', () => {
expect(formatPrice(15000)).toContain('Rp');
});
});Kapan unit test cocok
- logika murni yang tidak perlu DOM, network, atau filesystem
- transformasi data
- validasi aturan bisnis
- decision logic yang punya banyak cabang
Kelebihan unit test
- cepat
- feedback cepat
- mudah diisolasi saat gagal
- cocok untuk regression protection pada logic yang sering diubah
Batasan unit test
- bisa terlalu “rapi” kalau cuma menguji implementasi internal
- tidak menangkap masalah integrasi antarlayer
- bisa memberi rasa aman palsu kalau mock terlalu banyak dan real behavior tidak pernah disentuh
Integration testing
Integration testing menguji bagaimana beberapa bagian bekerja bersama. Di JavaScript, ini bisa berarti komponen + state + event handler, service + fetch wrapper + parser, atau route handler + validation + response helper.
Ciri integration test
- lebih dari satu bagian ikut berperan
- dependency bisa sebagian nyata, sebagian dimock
- fokus pada interaksi antarbagian, bukan detail kecil implementasi
- lebih dekat ke perilaku sistem nyata
Contoh integration test
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { UserSearch } from './UserSearch';
global.fetch = vi.fn();
describe('UserSearch', () => {
it('menampilkan hasil setelah user submit', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: 1, name: 'Andi' }],
});
render(<UserSearch />);
fireEvent.change(screen.getByLabelText(/cari user/i), {
target: { value: 'andi' },
});
fireEvent.click(screen.getByRole('button', { name: /cari/i }));
await waitFor(() => {
expect(screen.getByText('Andi')).toBeInTheDocument();
});
});
});Kapan integration test cocok
- saat behavior melibatkan lebih dari satu lapisan
- saat kamu ingin memastikan request, parsing, dan rendering tetap sinkron
- saat boundary kontrak antar modul penting
- saat bug sering muncul di titik sambungan, bukan di fungsi tunggal
Bedanya unit vs integration
| Aspek | Unit Test | Integration Test |
|---|---|---|
| Fokus | satu unit kecil | beberapa bagian yang bekerja bersama |
| Dependency | diminimalkan | sebagian nyata, sebagian boleh dimock |
| Kecepatan | paling cepat | lebih lambat |
| Diagnosis saat gagal | paling mudah | sedikit lebih kompleks |
| Nilai utama | validasi logika kecil | validasi koneksi antarbagian |
Kalau kamu ragu, tanyakan satu hal sederhana: apakah test ini memverifikasi logika internal, atau kerja sama antarbagian? Kalau yang diuji adalah kerja sama, itu sudah masuk wilayah integration test.
Mocking
Mocking adalah teknik mengganti dependency asli dengan versi kontrol. Tujuannya bukan menipu test, tapi membuat test fokus pada perilaku yang ingin divalidasi.
Kapan mocking dipakai
Mock dipakai saat dependency:
- lambat
- tidak stabil
- punya efek samping
- memanggil network
- menyentuh filesystem
- sulit diprediksi
- terlalu mahal untuk test berulang
Contoh dependency yang sering di-mock:
fetch- module API client
Date.now()Math.random()localStorage- fungsi logger
- payment gateway atau third-party SDK
Prinsip mocking yang sehat
- mock hanya dependency yang memang perlu dikontrol
- jangan mock terlalu banyak sampai test kehilangan makna
- mock perilaku eksternal, bukan logic yang sebenarnya ingin diuji
- reset mock antar test supaya state tidak bocor
Cara mocking fetch
Mocking fetch adalah kebutuhan yang sangat umum karena network call tidak cocok dijadikan dependency nyata di unit test.
Contoh sederhana
import { getUser } from './getUser';
global.fetch = vi.fn();
describe('getUser', () => {
beforeEach(() => {
fetch.mockReset();
});
it('mengambil data user', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'Dina' }),
});
const user = await getUser(1);
expect(fetch).toHaveBeenCalledWith('/api/users/1');
expect(user.name).toBe('Dina');
});
});Pola mock fetch yang perlu diperhatikan
fetch.mockResolvedValueOnce(...)untuk response sukses bertahapfetch.mockRejectedValueOnce(...)untuk network failureok: falseuntuk simulasi HTTP errorjson: async () => ...untuk body JSON
Contoh error branch
fetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
});Kalau code kamu memeriksa response.ok, test harus meniru shape response yang sama. Jangan cukup mengembalikan object kosong kalau code produksi butuh properti tertentu.
Cara mocking module
Mocking module dipakai ketika kamu ingin mengganti satu modul dependency dengan versi palsu dalam test. Ini umum untuk API client, helper yang memanggil network, atau module yang side-effect-nya tidak ingin jalan.
Contoh mocking module di Vitest
vi.mock('./apiClient', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
}));import { apiClient } from './apiClient';
import { loadProfile } from './loadProfile';
describe('loadProfile', () => {
it('memakai apiClient yang dimock', async () => {
apiClient.get.mockResolvedValueOnce({ data: { name: 'Rani' } });
const result = await loadProfile();
expect(apiClient.get).toHaveBeenCalledWith('/profile');
expect(result.name).toBe('Rani');
});
});Contoh mocking module di Jest
jest.mock('./apiClient', () => ({
apiClient: {
get: jest.fn(),
},
}));Kapan module mock dipakai
- modul punya side effect saat di-import
- modul membungkus dependency luar
- kamu ingin mengganti behavior tanpa mengubah kode produksi
- test butuh kontrol penuh atas output dependency
Jebakan module mocking
- mock terlalu dalam sampai test cuma memverifikasi mock, bukan perilaku
- path import tidak konsisten sehingga mock tidak kena
- state mock bocor antar test karena tidak di-reset
- module di-cache dan hasil import lama tetap dipakai kalau setup-nya salah
Test structure
Struktur test yang bagus membuat test lebih mudah dipindai, lebih mudah dirawat, dan lebih gampang dipahami saat gagal.
Pola umum: Arrange - Act - Assert
describe('sum', () => {
it('menjumlahkan dua angka', () => {
// Arrange
const a = 2;
const b = 3;
// Act
const result = sum(a, b);
// Assert
expect(result).toBe(5);
});
});Kenapa struktur ini dipakai
- Arrange: siapkan data, mock, atau precondition
- Act: jalankan satu aksi utama
- Assert: verifikasi hasil
Pola ini membantu test tetap fokus. Kalau satu test mulai berisi banyak aksi, biasanya dia perlu dipecah.
Struktur nama test yang baik
Nama test sebaiknya menyebut perilaku, bukan detail implementasi.
Contoh yang bagus:
menampilkan error saat request gagalmengembalikan user aktif dari daftarmenolak password yang terlalu pendek
Contoh yang kurang bagus:
harus memanggil function Xtes bagian 1works
Flow execution pada test async
Async test sering gagal bukan karena logikanya salah, tapi karena alur timing-nya tidak diikuti dengan benar.
Kenapa flow ini penting
Kalau kamu tidak await, assertion bisa berjalan sebelum operasi async selesai. Hasilnya test bisa:
- pass padahal seharusnya fail
- fail secara flake
- tidak menunggu state update selesai
- berhenti di tengah promise chain
Jebakan umum pada async tests
1) Lupa await
it('mengambil data', () => {
const result = fetchData();
expect(result).toBe(...);
});Kalau fetchData() mengembalikan Promise, test di atas salah karena assertion dilakukan sebelum hasil tersedia.
Perbaikan:
it('mengambil data', async () => {
const result = await fetchData();
expect(result).toBe(...);
});2) Menunggu dengan cara yang salah
Kalau UI update terjadi setelah state berubah, gunakan await waitFor(...) atau query async yang sesuai. Jangan pakai setTimeout sembarangan hanya untuk “menunggu”.
3) Tidak menangkap rejection
Jika function memang seharusnya melempar, test harus menuliskannya dengan jelas.
await expect(loadProfile()).rejects.toThrow('Gagal');4) Assertion di luar jalur async
Kadang callback async selesai setelah test sudah dianggap selesai. Gejalanya: test hijau padahal callback tidak pernah diverifikasi.
5) Mock state bocor antar test
Kalau mock tidak di-reset, hasil test sebelumnya bisa memengaruhi test berikutnya.
Biasakan:
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});atau padanan di Jest sesuai kebutuhan.
6) Terlalu bergantung pada timing internal
Jangan menguji “berapa lama” sesuatu selesai kecuali memang itu yang penting. Uji hasil akhirnya, bukan implementasi delay-nya.
Catatan implementasi
Pilih framework sesuai ekosistem
Di JavaScript, pola test bisa dijalankan dengan beberapa tool:
- Jest: sangat umum di banyak codebase
- Vitest: populer di project Vite dan modern frontend stack
- Node test runner: cocok untuk kebutuhan runtime Node yang lebih minimal
Yang penting bukan merek alatnya, tapi konsistensi struktur dan cara membaca hasil test.
Rekomendasi praktis
- unit test untuk logic murni
- integration test untuk boundary penting
- mock
fetchdan module yang berhubungan dengan dependency luar - gunakan
describeuntuk mengelompokkan behavior - satu test = satu fokus perilaku
- reset mock dan state antar test
- buat test name yang bisa dibaca tanpa membuka kode implementasi
Saat test mulai sulit ditulis
Biasanya ada tiga kemungkinan:
- fungsi terlalu besar
- dependency terlalu bercampur
- boundary belum jelas
Dalam kasus seperti itu, test sering lebih mudah ditulis setelah code dirapikan, bukan sebaliknya.
Kesalahan umum
- semua dependency dimock sampai test tidak lagi mewakili perilaku nyata
- integration test dicampur dengan banyak assertion kecil yang tidak berhubungan
- async test tidak memakai
await - menunggu hasil UI dengan cara manual yang rapuh
- test name hanya menjelaskan langkah, bukan perilaku
- mock tidak di-reset sehingga suite jadi flaky
- unit test dipaksa menguji hal yang sebenarnya lebih cocok diuji di integration test
Referensi terkait
Event Loop, Promise, Microtask, dan async/awaitModules JavaScript: import/export, Default vs Named Export, Dynamic Import, dan Struktur File yang SehatFetch API, Async Data Flow, JSON Parsing, dan Error Handling DasarError Handling di JavaScript: try/catch, throw, Custom Error, dan Debugging Mindset