Testing di React: Jest, React Testing Library, dan Best Practices

Jujur aja, dulu aku paling males nulis test. Rasanya buang-buang waktu — udah capek nulis fitur, harus nulis kode lagi buat nge-tes kode yang baru aja ditulis? Serius?

Tapi setelah beberapa kali dapet bug di production yang seharusnya bisa dicegah, aku mulai sadar: testing itu bukan beban, tapi investasi. Dan kalau kamu baca sampai sini, kemungkinan besar kamu juga lagi mulai mikir hal yang sama.

Artikel ini aku tulis bukan kayak textbook. Ini lebih kayak aku lagi ngobrol sama kamu, berbagi pengalaman gimana caranya mulai nulis test di React dari nol, pakai Jest dan React Testing Library, sampai ke best practices yang aku pelajari (banyak) dari trial and error.


Kenapa Testing Itu Penting (Terutama di React)?

Sebelum masuk ke kode, penting banget buat ngerti kenapa kita nulis test. Soalnya kalau alasan kamu cuma “katanya harus” atau “biar senior nggak marah”, nggak akan tahan lama ngejalaninnya.

React itu, pada dasarnya, library yang sangat declarative. Kamu bilang “ini tampilannya”, dan React yang urus kapan render-nya. Masalahnya, semakin kompleks state management dan interaksi user, semakin besar chance ada edge case yang nggak kamu pikirin.

Test membantu kamu:

  • Catch bug lebih awal — sebelum QA atau user yang nemuin.
  • Refactor dengan percaya diri — mau ubah struktur komponen? Kalau test masih hijau, aman.
  • Dokumentasi hidup — test yang baik menjelaskan expected behavior dari kode kamu.
  • Tidur lebih nyenyak — nggak ada lagi drama “kok di production malah blank page?”

Dan di era CI/CD kayak sekarang, test suite yang kuat itu benteng pertahanan terakhir sebelum kode kamu deploy ke production.


Kenalan Sama Jest

Jest adalah test runner buatan Facebook (sekarang Meta) yang udah jadi default kalau kamu bikin React project pakai Create React App atau Vite (dengan config tambahan). Dia menangani segala hal soal eksekusi test, assertion, mocking, dan coverage report.

Setup Dasar

Kalau kamu pakai Create React App, Jest udah include. Tapi kalau pakai Vite, kamu perlu install manual:

npm install -D jest @types/jest ts-jest

Lalu tambah script di package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Nulis Test Pertama

Mari mulai dari yang paling simpel. Misal kamu punya fungsi utility:

// utils.js
export function formatCurrency(amount) {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
    minimumFractionDigits: 0,
  }).format(amount);
}

Test-nya kayak gini:

// utils.test.js
import { formatCurrency } from './utils';

describe('formatCurrency', () => {
  test('memformat angka menjadi format Rupiah', () => {
    expect(formatCurrency(150000)).toBe('Rp\u00A0150.000');
  });

  test('menghandle angka 0', () => {
    expect(formatCurrency(0)).toBe('Rp\u00A00');
  });

  test('menghandle angka besar', () => {
    expect(formatCurrency(1000000000)).toBe('Rp\u00A01.000.000.000');
  });
});

Kalau kamu jalankan npm test, hasilnya hijau semua. Keren, kamu baru aja nulis test pertama!

Matchers yang Sering Dipake

Jest punya banyak matchers, tapi yang paling sering aku pakai:

expect(value).toBe(exactValue)       // strict equality (===)
expect(value).toEqual(deepValue)      // deep equality (object/array)
expect(value).toBeTruthy()            // truthy check
expect(value).toContain(item)         // array/string contains
expect(fn).toThrow()                  // function throws error
expect(value).toMatchSnapshot()       // snapshot comparison

Salah satu kesalahan yang sering aku lihat di kode orang: pakai .toBe() buat nge-tes object. Ingat, .toBe() itu reference equality, jadi {a: 1} === {a: 1} itu false. Pakai .toEqual() buat object dan array.


React Testing Library: Filosofi yang Mengubah Cara Kamu Nulis Test

Kalau Jest itu test runner-nya, React Testing Library (RTL) itu yang bantu kamu ngetes komponen React. Dan dia punya filosofi yang cukup revolusioner:

“The more your tests resemble the way your software is used, the more confidence they can give you.” — Kent C. Dodds

Artinya: jangan ngetes implementasi detail, ngetes perilaku.

Dulu (zaman Enzyme), kita sering ngetes kayak gini:

// ❌ Cara lama (Enzyme) — ngetes implementasi
const wrapper = shallow(<Counter />);
expect(wrapper.state('count')).toBe(0);
wrapper.find('button').simulate('click');
expect(wrapper.state('count')).toBe(1);

Masalahnya? Kalau kamu refactor state pakai useReducer atau state management lain, test-nya pecah padahal perilakunya sama.

Dengan React Testing Library:

// ✅ Cara baru — ngetes perilaku
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('menampilkan counter dan bisa increment', () => {
  render(<Counter />);

  expect(screen.getByText('Count: 0')).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /tambah/i }));

  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Notice bedanya? Kita ngetes kayak user beneran make app-nya: lihat teks, klik tombol, lihat hasilnya berubah.

Setup React Testing Library

npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event

Tambahkan setup file (biasanya setupTests.js):

import '@testing-library/jest-dom';

Query Priority

RTL punya beberapa cara buat mencari elemen. Dan ada urutan prioritas yang disarankan:

  1. getByRole — paling disarankan, mencari berdasarkan ARIA role
  2. getByLabelText — cocok buat form
  3. getByPlaceholderText — kalau label nggak ada
  4. getByText — mencari berdasarkan teks yang terlihat
  5. getByDisplayValue — buat form input yang sudah terisi
  6. getByAltText — buat images
  7. getByTestId — pilihan terakhir kalau nggak ada cara lain
// Contoh beberapa query
render(<LoginForm />);

// ✅ Prioritas tinggi
screen.getByRole('button', { name: /login/i });
screen.getByLabelText(/email/i);

// ⚠️ Boleh, tapi kurang ideal
screen.getByText('Welcome back');

// ❌ Pilihan terakhir
screen.getByTestId('login-form');

Kenapa getByRole lebih baik? Karena kalau kamu bisa menemukan elemen lewat role-nya, berarti elemen itu punya aksesibilitas yang benar. Test yang bagus menghasilkan aksesibilitas yang bagus juga.

User Event vs Fire Event

Satu hal lagi yang penting: jangan pakai fireEvent kalau bisa pakai userEvent.

import userEvent from '@testing-library/user-event';

test('input form berfungsi', async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/nama/i), 'Budi');
  await user.click(screen.getByRole('button', { name: /kirim/i }));

  expect(screen.getByText(/berhasil/i)).toBeInTheDocument();
});

userEvent lebih realistis karena dia simulasi interaksi user yang sesungguhnya — keydown, keypress, keyup, focus, blur — bukan cuma trigger event begitu aja. Bedanya sering keliatan di edge case.


Testing Komponen yang Lebih Kompleks

Oke, test komponen statis itu gampang. Tapi gimana kalau komponennya punya API call, routing, atau global state?

Mocking API Call

Kalau komponen kamu fetch data dari API, kamu perlu mock fetch-nya:

// UserProfile.jsx
import { useState, useEffect } from 'react';

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (!user) return <p>User tidak ditemukan</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Test-nya:

import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';

// Mock global fetch
global.fetch = jest.fn();

test('menampilkan data user setelah fetch berhasil', async () => {
  fetch.mockResolvedValueOnce({
    json: async () => ({
      name: 'Andi',
      email: '[email protected]',
    }),
  });

  render(<UserProfile userId="1" />);

  expect(screen.getByText('Loading...')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('Andi')).toBeInTheDocument();
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
  });
});

test('menampilkan error ketika fetch gagal', async () => {
  fetch.mockResolvedValueOnce({
    json: async () => null,
  });

  render(<UserProfile userId="999" />);

  await waitFor(() => {
    expect(screen.getByText('User tidak ditemukan')).toBeInTheDocument();
  });
});

waitFor itu penting banget karena state update di React itu async. Tanpa waitFor, test bisa jalan sebelum state ter-update.

Testing Custom Hooks

React Testing Library juga punya renderHook buat ngetes custom hooks:

import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('useCounter bisa increment dan decrement', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement();
  });

  expect(result.current.count).toBe(0);
});

Best Practices yang Aku Pelajari (Biasanya dari Kesalahan)

Ini bagian yang paling penting menurutku. Bukan soal teknisnya, tapi cara pikir dan kebiasaan yang bikin test suite kamu benar-benar berguna.

1. Ngetes Perilaku, Bukan Implementasi

Ini udah aku singgung di atas, tapi worth diulang. Hindari:

// ❌ Jangan ngetes state internal
expect(component.state.isLoading).toBe(false);

// ❌ Jangan ngetes method internal
expect(component.instance().handleClick).toHaveBeenCalled();

Sebaliknya:

// ✅ Ngetes apa yang user lihat
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('Data berhasil dimuat')).toBeInTheDocument();

2. Test Suite Harus Cepat

Kalau test suite butuh 10 menit, developer nggak akan pernah jalankan secara lokal. Beberapa tips:

  • Jangan ngetes library pihak ketiga — trust mereka udah ngetes sendiri
  • Minimalkan snapshot test — mereka lambat dan sering bikin false positive
  • Mock network request — jangan panggil API beneran
  • Parallelisasi — Jest bisa jalankan test paralel

3. Setiap Test Harus Independen

Test nggak boleh bergantung pada test lain. Tiap test harus bisa dijalankan sendiri tanpa urutan tertentu:

// ✅ Setiap test punya setup sendiri
test('menambah item ke keranjang', () => {
  render(<Cart items={[]} />);
  // ...
});

test('menghapus item dari keranjang', () => {
  render(<Cart items={[mockItem]} />);
  // ...
});

4. Pakai data-testid Sebagai Pilihan Terakhir

Aku tahu, kadang males mikirin role atau label. Tapi pakai data-testid di mana-mana itu tanda komponen kamu mungkin nggak accessible. Tantang diri sendiri buat pakai query yang lebih semantik.

5. Testing Library: Jangan Import Langsung dari react-dom

Ini kesalahan yang sering aku lihat:

// ❌ Import dari react-dom
import { render } from 'react-dom';

// ✅ Import dari testing library
import { render } from '@testing-library/react';

Simple, tapi beda banget.

6. Tulis Test yang Readable

Test itu dibaca sama manusia, bukan cuma mesin. Pakai nama yang deskriptif:

// ❌ Kurang deskriptif
test('button', () => { ... });

// ✅ Jelas ekspektasinya
test('menampilkan pesan error ketika email kosong', () => { ... });

7. Coverage Bukan Segalanya

100% coverage bukan berarti 100% bug-free. Yang penting: test fitur-fitur kritis dan edge case yang realistis. Kalau kamu ngetes 80% tapi semua yang penting ter-cover, itu lebih baik daripada 100% tapi isinya test yang meaningless.


Struktur Folder yang Rapi

Pengalaman aku, struktur test yang paling nyaman:

s├│││││││├││└r───c───/c├│││└h├└u├└o──o──t──m──o──i──pkloB├├└C├└suusffnu───a──/ss/ooet───r──eerrntdAAmmtoBBB/CCuuaasnuuuaatttt//tttrrhhCCtttdd..uuooo..jtrrnnnjtserr...seseejtmxstnnseot.ccxsd.jyytujs...lsjtjexses.sxcts.sjs←testsebelahansamakomponen

Ini bikin kamu gampang nemuin test yang relevan, dan bikin mental model: setiap file kode punya file test yang mendampinginya.


Saatnya Kamu Mulai Nulis Test

Jangan langsung pengen cover semua. Mulai dari yang paling kritis:

  1. Utility functions — gampang ditest, no-brainer
  2. Critical user flows — login, checkout, submit form
  3. Complex components — yang punya banyak kondisi dan state
  4. Edge cases — error handling, empty state, loading state

Dan ingat: test yang buruk lebih baik daripada nggak ada test sama sekali. Kamu bisa improve seiring waktu. Yang penting mulai.


Butuh Bantuan Lebih Lanjut?

Kalau kamu lagi butuh konsultasi soal testing strategy di project React kamu, atau butuh bantuan setup CI/CD pipeline dengan test suite yang proper, jangan ragu buat hubungi aku di [email protected]. Senang bisa bantu!


FAQ (Pertanyaan yang Sering Ditanyain)

Apakah React Testing Library menggantikan Jest?

Nggak. Mereka bekerja bersama. Jest adalah test runner yang menjalankan test, menyediakan assertion (expect), dan mocking. React Testing Library berfungsi sebagai helper untuk me-render komponen React dan memberikan utility untuk query elemen. Keduanya saling melengkapi — kamu butuh keduanya.

Berapa persen ideal test coverage untuk project React?

Tidak ada angka ajaib. Yang lebih penting daripada persentase adalah kualitas test. Tim yang aku kenal biasanya target di 70–80% untuk komponen dan logic bisnis, tapi nggak narget 100% untuk hal-hal trivial seperti styling atau third-party wrapper. Lebih baik 60% yang meaningful daripada 100% yang isinya test test kosong.

Bagaimana cara ngetes komponen yang pakai React Context atau Redux?

Bungkus komponen kamu dengan Provider yang berisi mock data atau store:

import { Provider } from 'react-redux';
import { store } from './store';

test('komponen dengan Redux', () => {
  render(
    <Provider store={store}>
      <MyComponent />
    </Provider>
  );
  // assertions...
});

Untuk Context, bikin wrapper function yang reusable supaya kamu nggak perlu nulis Provider berulang-ulang di setiap test.

Kapan sebaiknya pakai mock vs integrasi test sungguhan?

Mock untuk unit test komponen individual yang butuh isolasi (API call, third-party service). Integration test tanpa mock untuk memastikan beberapa komponen bekerja bersama dengan benar. Idealnya kamu punya keduanya: unit test yang cepat dan banyak, plus beberapa integration test yang cover critical path user secara end-to-end.


Selamat nulis test! Ingat, setiap test yang kamu tulis adalah investasi untuk tidurmu yang lebih nyenyak malam ini. 😴