17. Unit & Behavioral Testing

Pengujian perangkat lunak di sisi frontend terbagi menjadi beberapa level, dengan Unit Testing (menguji unit terkecil seperti fungsi utilitas, helper, atau custom hooks) dan Behavioral Testing (menguji bagaimana komponen berinteraksi dengan pengguna di layar) sebagai pilar utama.

Tujuan utama pengujian ini bukan sekadar mengejar angka code coverage, melainkan untuk:

  1. Mencegah Regresi: Menjamin perubahan kode di masa mendatang tidak merusak fitur yang sudah berjalan (working code).
  2. Dokumentasi Hidup: Test case berfungsi sebagai spesifikasi teknis nyata tentang bagaimana suatu komponen seharusnya berperilaku.
  3. Meningkatkan Arsitektur: Komponen yang sulit dites biasanya mengindikasikan kode yang terlalu kompleks (tightly coupled), sehingga memaksa developer menulis kode yang lebih modular (loosely coupled).

A. Pengujian pada Vite + React JS (Vitest + React Testing Library)

Untuk proyek berbasis Vite, Vitest adalah standar utama yang sangat direkomendasikan sebagai test runner karena memiliki integrasi native dengan pipeline konfigurasi Vite (vite.config.ts), mendukung ESM secara out-of-the-box, dan jauh lebih cepat dibanding Jest tradisional.

⚙️ Konfigurasi Setup (vitest.config.ts atau vite.config.ts)

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts', // file inisialisasi testing library
    css: true, // aktifkan jika perlu menguji class CSS/visibility
  },
});

⚙️ File Setup (src/setupTests.ts)

import '@testing-library/jest-dom'; // Menyediakan custom matchers seperti toBeInTheDocument()
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// Bersihkan DOM setelah setiap pengujian selesai
afterEach(() => {
  cleanup();
});

📝 Contoh Unit & Behavioral Test (Vite + React)

Berikut adalah contoh komponen Counter sederhana dan pengujian perilakunya menggunakan Vitest + React Testing Library:

// Counter.tsx
import React, { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2 data-testid="count-val">Count: {count}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
// Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import Counter from './Counter';

describe('Counter Component (Vite + Vitest)', () => {
  it('harus merender nilai awal sama dengan 0', () => {
    render(<Counter />);
    const countElement = screen.getByTestId('count-val');
    expect(countElement).toHaveTextContent('Count: 0');
  });

  it('harus menambah nilai count saat tombol Increment diklik', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const incrementButton = screen.getByRole('button', { name: /increment/i });
    const countElement = screen.getByTestId('count-val');

    await user.click(incrementButton);
    expect(countElement).toHaveTextContent('Count: 1');
  });

  it('harus mengurangi nilai count saat tombol Decrement diklik', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const decrementButton = screen.getByRole('button', { name: /decrement/i });
    const countElement = screen.getByTestId('count-val');

    await user.click(decrementButton);
    expect(countElement).toHaveTextContent('Count: -1');
  });
});

B. Pengujian pada Next.js (Jest + React Testing Library)

Next.js memiliki compiler bawaan berbasis Rust (SWC) yang telah terintegrasi secara resmi dengan Jest. Konfigurasi ini sangat optimal untuk menangani fitur-fitur khusus Next.js seperti Server Components, Client Components, routing (next/navigation), dan optimasi gambar (next/image).

⚙️ Konfigurasi Setup (jest.config.js / jest.config.mjs)

import nextJest from 'next/jest.js';

const createJestConfig = nextJest({
  // Tentukan path ke aplikasi Next.js Anda agar dapat memuat env local
  dir: './',
});

// Konfigurasi Jest kustom
const config = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    // Menangani alias path seperti @/components/
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

export default createJestConfig(config);

⚙️ File Setup (jest.setup.js)

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

📝 Contoh Unit & Behavioral Test (Next.js)

Sering kali di Next.js kita harus melakukan mocking pada router atau hook navigasi seperti useRouter atau usePathname dari next/navigation.

// UserCard.tsx (Client Component)
'use client';

import { useRouter } from 'next/navigation';

interface UserCardProps {
  id: string;
  name: string;
}

export default function UserCard({ id, name }: UserCardProps) {
  const router = useRouter();

  return (
    <div className="p-4 border rounded shadow">
      <h3>{name}</h3>
      <button 
        onClick={() => router.push(`/users/${id}`)}
        className="text-blue-500 hover:underline"
      >
        Lihat Detail
      </button>
    </div>
  );
}
// UserCard.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UserCard from './UserCard';

// Melakukan mock modul next/navigation
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({
  useRouter() {
    return {
      push: mockPush,
      prefetch: () => null,
    };
  },
}));

describe('UserCard Component (Next.js + Jest)', () => {
  beforeEach(() => {
    mockPush.mockClear();
  });

  it('harus merender nama pengguna dengan benar', () => {
    render(<UserCard id="123" name="John Doe" />);
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });

  it('harus melakukan redirect ke halaman detail saat tombol diklik', async () => {
    const user = userEvent.setup();
    render(<UserCard id="123" name="John Doe" />);

    const detailButton = screen.getByRole('button', { name: /lihat detail/i });
    await user.click(detailButton);

    expect(mockPush).toHaveBeenCalledWith('/users/123');
    expect(mockPush).toHaveBeenCalledTimes(1);
  });
});

C. Snapshot Testing (Jest & Vitest)

Snapshot Testing adalah teknik pengujian di mana output render HTML/JSON dari suatu komponen disimpan dalam bentuk file teks representatif (.snap). Pada pengujian berikutnya, Jest/Vitest akan membandingkan output render saat ini dengan file snapshot yang disimpan. Jika ada perubahan visual atau struktur DOM, pengujian akan gagal dan developer dipaksa memverifikasi apakah perubahan tersebut disengaja atau merupakan bug.

📝 Contoh Penulisan Snapshot Test (Jest / Vitest)

// Button.tsx
import React from 'react';

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  children: React.ReactNode;
}

export default function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button className={`btn-${variant} px-4 py-2 rounded`}>
      {children}
    </button>
  );
}
// Button.test.tsx
import { render } from '@testing-library/react';
import Button from './Button';

describe('Button Snapshot', () => {
  it('harus cocok dengan snapshot untuk variant primary', () => {
    const { asFragment } = render(<Button variant="primary">Click me</Button>);
    // Menyimpan atau mencocokkan snapshot DOM
    expect(asFragment()).toMatchSnapshot();
  });

  it('harus cocok dengan snapshot untuk variant secondary', () => {
    const { asFragment } = render(<Button variant="secondary">Cancel</Button>);
    expect(asFragment()).toMatchSnapshot();
  });
});

📁 Hasil File Snapshot (__snapshots__/Button.test.tsx.snap)

File ini akan digenerate otomatis oleh Jest/Vitest pada folder __snapshots__:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Button Snapshot harus cocok dengan snapshot untuk variant primary 1`] = `
<DocumentFragment>
  <button
    class="btn-primary px-4 py-2 rounded"
  >
    Click me
  </button>
</DocumentFragment>
`;

exports[`Button Snapshot harus cocok dengan snapshot untuk variant secondary 1`] = `
<DocumentFragment>
  <button
    class="btn-secondary px-4 py-2 rounded"
  >
    Cancel
  </button>
</DocumentFragment>
`;

🔄 Cara Memperbarui Snapshot

Jika Anda sengaja mengubah styling atau markup komponen (misal menambahkan class margin m-2 pada button), test snapshot akan gagal. Anda wajib memperbarui file snapshot menggunakan flag -u:

  • Jest: npm run test -- -u atau jest -u
  • Vitest: vitest -u atau npx vitest run -u

🟢 Keunggulan Snapshot Testing (Advantages)

  • Setup Sangat Cepat: Menulis asersi untuk struktur HTML yang kompleks hanya butuh satu baris kode (toMatchSnapshot()).
  • Mendeteksi Perubahan Tak Disengaja: Sangat efektif mendeteksi regresi pada komponen murni visual (UI-only components) seperti Icon, Badge, Typography, atau Card statis.
  • Mencegah Kerusakan Markup: Menjamin struktur DOM tidak bergeser secara tidak sadar akibat refactoring internal.

🔴 Kekurangan Snapshot Testing (Disadvantages)

  • False Confidence: Developer cenderung menekan jest -u secara terburu-buru tanpa memverifikasi apakah perubahan DOM tersebut aman atau merusak fungsionalitas UI.
  • Polusi File Repository: Setiap kali ada perubahan kecil di UI, file .snap yang berukuran besar akan terupdate, membuat Pull Request (PR) penuh dengan baris perubahan snapshot yang sulit ditinjau secara manual.
  • Kurang Efektif untuk Komponen Dinamis: Komponen yang merender data acak (UUID, tanggal saat ini new Date(), math random) akan selalu gagal dalam pengujian snapshot kecuali data tersebut dimock terlebih dahulu.

D. Tabel Perbandingan Test Runner: Vitest vs Jest

KriteriaVitest ⭐Jest
Ekosistem UtamaVite-native (sangat cocok untuk project Vite)Tradisional (Webpack, Babel, Next.js)
Kecepatan RunSangat Cepat (menggunakan esbuild & worker threads)Lambat untuk cold-start (perlu kompilasi Babel/TS-Jest)
Dukungan ESMBawaan (Native ESM)Terbatas / Butuh konfigurasi manual tambahan
HMR (Hot Module Replacement)Ya (Mendukung watch mode instan tanpa merestart test)Tidak ada
KonfigurasiBerbagi file config dengan Vite (vite.config.ts)Terpisah (jest.config.js)
API CompatibilityKompatibel penuh dengan sintaks Jest (describe, test, expect)Standar industri terpopuler

E. Best Practices Unit & Behavioral Testing

  1. Gunakan @testing-library/user-event daripada fireEvent

    • fireEvent memicu event browser secara instan (fireEvent.click) tanpa menjalankan seluruh rangkaian event browser (seperti focus, hover, keydown, keyup).
    • user-event menyimulasikan interaksi nyata manusia di browser secara asinkron.
  2. Jangan Menguji Detail Implementasi (Testing Implementation Details)

    • Jangan menguji internal state React (seperti useState value secara langsung) atau private methods.
    • Ujilah apa yang dilihat dan dialami oleh pengguna di layar (misalnya: "apakah teks berubah setelah tombol diklik?"). Hal ini membuat test suite Anda tidak mudah patah saat refactoring internal.
  3. Gunakan Mock Service Worker (MSW) untuk API Network Mocking

    • Hindari me-mock fetch atau axios secara global secara manual menggunakan jest.spyOn.
    • Gunakan MSW untuk memotong request jaringan di tingkat jaringan tiruan. Hal ini menjaga test case tetap realistis seolah-olah berinteraksi dengan API sungguhan.
  4. Kapan Harus Memilih Jenis Testing?

    • Unit Test: Tepat untuk utility pure functions, helper format tanggal, kalkulator pajak, atau custom hooks bisnis.
    • Behavioral (Integration) Test: Tepat untuk interaksi antar komponen React, pengisian formulir, validasi form, navigasi halaman.
    • Snapshot Test: Tepat untuk komponen statis dengan markup DOM stabil (UI/design system components) yang jarang berubah perilakunya namun rawan terhadap pergeseran visual.

F. Referensi Dokumentasi Resmi (Official Documentation Links)

Untuk mempelajari lebih lanjut dan mengikuti petunjuk resmi mengenai setup serta API terbaru dari masing-masing pustaka pengujian, silakan merujuk ke dokumentasi resmi berikut:


Rekomendasi:

  1. Gunakan Vitest + React Testing Library sebagai standar utama pengujian proyek Vite + React JS karena integrasi native-nya yang sangat cepat dan ringan.
  2. Gunakan Jest + React Testing Library untuk proyek Next.js karena didukung secara resmi oleh Next.js via compiler Rust (next/jest).
  3. Gunakan Snapshot Testing secara selektif (hanya pada komponen visual statis seperti Icon, Badge, atau komponen murni UI/Design System). Hindari snapshot testing pada komponen dinamis atau halaman lengkap untuk mencegah false confidence dan polusi berkas snapshot pada repositori.
  4. Selalu gunakan @testing-library/user-event daripada fireEvent untuk mensimulasikan interaksi nyata pengguna di browser secara akurat.

© 2026 Bit Group Asia. All rights reserved.

ArchitectureSecurityGuidelines