Apa itu Unit Testing dan Mengapa Penting
Hey teman-teman developer! Kali ini kita akan bahas topik yang mungkin sering dihindari sama pemula, yaitu unit testing. Tapi tenang aja, gue akan jelasin dengan cara yang santai dan mudah dipahami.
Unit testing itu basicaly cara kita nguji bagian-bagian kecil dari kode kita secara terpisah. Bayangin aja kayak kamu lagi rakit motor, sebelum dipasang semua, kamu pasti mau mastiin dulu kan setiap part-nya berfungsi dengan baik? Nah, unit testing itu konsepnya mirip kayak gitu.
Dalam konteks React, unit testing berarti kita nguji komponen-komponen kita satu per satu. Misalnya kamu punya tombol, form, atau card component, kita bisa tes apakah komponen tersebut render dengan benar, apakah event handler-nya berfungsi, dan apakah state management-nya berjalan sesuai ekspektasi.
Kenapa sih testing itu penting? Pertama, testing bikin kamu lebih percaya diri waktu push code ke production. Kedua, testing juga bisa jadi dokumentasi hidup untuk kode kamu - orang lain bisa liat test dan langsung paham gimana cara pake komponen tersebut. Ketiga, testing membantu kamu catch bug lebih awal sebelum user nemuin. Dan yang terakhir, testing bikin proses refactoring jadi lebih aman karena kamu tau kalau ada yang rusak.
Keuntungan Vite untuk Testing
Sekarang, kenapa kita pake Vite buat testing? Kalau kamu udah familiar sama Vite, pasti tau dong kalo dia itu super cepat buat development. Nah, kelebihan ini juga berlaku buat testing environment.
Vite punya build tool yang berbasis ES modules dan menggunakan esbuild untuk transpilasi, yang bikin prosess testing jadi jauh lebih cepat dibanding tools tradisional kayak Create React App yang pake Jest dengan Webpack. Waktu kamu ngejalanin test, Vite bisa nge-cache modul-modul yang udah di-load, jadi test selanjutnya jalan lebih kencang.
Yang lebih keren lagi, Vite punya hot module replacement (HMR) yang bisa kita manfaatin buat test juga. Jadi waktu kamu ubah test atau komponen, dia langsung re-run test yang relevan aja, nggak perlu ngejalanin semua test dari awal.
Configurasi Vite juga lebih simpel dan straightforward. Kamu nggak perlu setup Webpack config yang ribet atau ngatur berbagai macam loader. Sebagian besar konfigurasi udah di-handle sama Vite secara otomatis, jadi kamu bisa fokus nulis test yang berkualitas.
Tools yang Akan Digunakan: Vitest, React Testing Library
Untuk tutorial ini, kita akan pake tiga tools utama yang udah jadi standar industri buat React testing. Let me jelasin satu per satu:
Vitest - Test Runner yang Kencang
Yang pertama adalah Vitest. Ini adalah test runner yang dibuat khusus untuk Vite ecosystem. Yang bikin dia istimewa adalah:
- Kompatibel dengan Jest API, jadi kalau kamu udah familiar sama Jest, transisinya bakal smooth banget
- Punya watch mode yang cerdas - cuma nge-run test yang berubah
- Coverage reporting yang detail buat liat seberapa banyak kode yang udah ke-test
- UI mode yang interaktif, jadi kamu bisa liat hasil test dengan tampilan yang lebih enak
React Testing Library - Test Like a User
Kedua, kita akan pake React Testing Library. Library ini punya filosofi yang unik: "test your software the way your users use it".
Maksudnya gimana? Instead of nguji internal state atau method yang user nggak liat, kita nguji apakah komponen kita behave sesuai ekspektasi user. Contohnya:
- User klik tombol, apakah sesuatu terjadi?
- User input data ke form, apakah data tersimpan dengan benar?
- User scroll ke bawah, apakah konten baru muncul?
React Testing Library juga encourage kita untuk nulis test yang lebih maintainable dan less fragile. Misalnya, daripada cari element berdasarkan class name yang bisa berubah, lebih baik cari berdasarkan text content atau accessibility attributes.
@testing-library/jest-dom - Assertion yang Semantic
Yang ketiga adalah @testing-library/jest-dom. Ini adalah extension yang nambain custom matchers untuk Jest/Vitest, yang bikin assertion kita lebih readable dan semantic.
Misalnya instead of nulis expect(element.style.display).toBe('none'), kita bisa nulis expect(element).not.toBeVisible(). Lebih jelas kan maksudnya?
Siap buat mulai?
Kombinasi ketiga tools ini bakal ngasih kamu foundation yang solid buat nulis test React yang berkualitas tinggi. Di tutorial selanjutnya, kita akan setup environment dan mulai nulis test pertama kita.
Install React dengan Vite
Sebelum kita mulai ngulik testing, kita perlu bikin project React dulu dong. Kali ini kita akan pake Vite dengan TypeScript yang udah terbukti performanya mantap buat development.
Cara membuat project React TypeScript
Kita akan langsung setup project dengan TypeScript karena ini best practice buat project modern. Ada beberapa pilihan command yang bisa kamu pakai:
Metode 1: Pakai npx (paling reliable)
npx create-vite@latest buildwithangga-testing --template react-ts

Metode 2: Interactive CLI (recommended buat pemula)
npm create vite@latest
Lalu ikuti prompt berikut:
- Project name: ketik
buildwithangga-testing - Select framework: pilih
React - Select variant: pilih
TypeScript
Metode 3: Menggunakan yarn
yarn create vite buildwithangga-testing --template react-ts
Masuk ke project dan install
Setelah project terbuat, masuk ke foldernya dan install dependencies:
cd buildwithangga-testing
npm install
Verifikasi project berjalan
Test apakah project udah berjalan dengan baik:
npm run dev
Buka browser ke http://localhost:5173 dan pastikan halaman React muncul dengan baik.

Keuntungan pakai TypeScript dari awal
TypeScript memberikan beberapa benefit penting buat testing:
- Type checking otomatis yang bikin bug lebih mudah ketahuan
- IntelliSense yang lebih baik di IDE
- Self-documenting code dengan interface dan types
- Refactoring yang lebih aman
Install Testing Dependencies
Sekarang waktunya install semua library yang dibutuhin buat testing React dengan TypeScript.
Install package testing
Jalankan command ini buat install semua dependencies sekaligus:
npm install -D vitest @testing-library/dom @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @types/jest
Breakdown setiap package:
vitest - Test runner yang dibuat khusus buat Vite. Compatible dengan Jest API tapi jauh lebih cepat.
@testing-library/react - Library utama buat render dan test React components. Fokus ke user behavior, bukan implementation details.
@testing-library/jest-dom - Custom matchers yang bikin assertions lebih readable, kayak toBeVisible(), toHaveClass(), etc.
@testing-library/user-event - Simulate user interactions dengan cara yang lebih realistic dibanding fireEvent.
jsdom - DOM implementation buat Node.js environment, karena test jalan di Node bukan browser.
@types/jest - Type definitions buat Jest/Vitest functions. Penting buat TypeScript supaya recognize describe, it, expect.
Cek instalasi berhasil
Pastikan semua package masuk ke devDependencies di package.json. Kamu harusnya liat semua package di atas tercantum dengan benar.

Konfigurasi vite.config.ts
File konfigurasi Vite perlu diupdate buat support testing environment dengan TypeScript.
Update vite.config.ts
Buka file vite.config.ts dan ganti isinya dengan:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
// <https://vitejs.dev/config/>
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
css: true,
},
});
Buka file tsconfig.app.json dan ubah jadi seperti ini:
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Custom */
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src"]
}
Penjelasan konfigurasi testing:
globals: true - Allows pake describe, it, expect without explicit imports di setiap test file.
environment: 'jsdom' - Set jsdom sebagai browser environment simulator.
setupFiles - File yang dijalanin sebelum semua test. Biasanya buat global config atau imports.
css: true - Enable CSS processing di test environment.
types: ['vitest/globals'] - Tambahan type untuk TypeScript supaya recognize Vitest globals.
Buat setup file
Buat folder test di dalam src dan file setup.ts:
mkdir src/test
Isi file src/test/setup.ts:
import '@testing-library/jest-dom'
File ini simple tapi crucial. Dengan import jest-dom, semua custom matchers jadi available di semua test files.
Tambah test scripts
Update package.json dengan scripts buat testing:
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
Penjelasan script:
test- Run tests dalam watch modetest:ui- Buka Vitest UI interface di browsertest:run- Run tests sekali aja tanpa watchtest:coverage- Run tests dengan coverage report
Struktur Folder Testing
Organization folder yang baik crucial buat maintainability testing code, apalagi dengan TypeScript.
Recommended folder structure
Setup struktur folder seperti ini buat project BuildWithAngga:

Naming conventions:
React components → .tsx dan .test.tsx
Utility functions → .ts dan .test.ts
Type definitions → .types.ts
Global types → Di folder types/
Letakkan test files sejajar dengan component yang dites. Ini bikin easier buat maintain dan mencari files.
Contoh component dengan proper TypeScript
Mari buat sample CourseCard component buat BuildWithAngga.
Pertama buat types di src/components/CourseCard/CourseCard.types.ts:
export interface CourseCardProps {
title: string
instructor: string
price: number
thumbnail: string
difficulty: 'Beginner' | 'Intermediate' | 'Advanced'
category?: string
rating?: number
onEnroll?: () => void
}
export interface CourseData {
id: string
title: string
instructor: string
price: number
thumbnail: string
difficulty: CourseCardProps['difficulty']
category: string
rating: number
enrollmentCount: number
}
Lalu buat component di src/components/CourseCard/CourseCard.tsx:
import type { CourseCardProps } from "./CourseCard.types";
import "./CourseCard.css";
const CourseCard: React.FC<CourseCardProps> = ({
title,
instructor,
price,
thumbnail,
difficulty,
category = "Programming",
rating = 0,
onEnroll,
}) => {
const handleEnrollClick = () => {
if (onEnroll) {
onEnroll();
}
};
return (
<div className="course-card" data-testid="course-card">
<div className="course-thumbnail">
<img src={thumbnail} alt={title} />
<span className={`difficulty ${difficulty.toLowerCase()}`}>
{difficulty}
</span>
</div>
<div className="course-content">
<div className="course-category">{category}</div>
<h3 className="course-title">{title}</h3>
<p className="course-instructor">by {instructor}</p>
{rating > 0 && (
<div className="course-rating">⭐ {rating.toFixed(1)}</div>
)}
<div className="course-price">
{price === 0 ? "Gratis" : `Rp ${price.toLocaleString()}`}
</div>
<button
className="enroll-button"
onClick={handleEnrollClick}
data-testid="enroll-button"
>
{price === 0 ? "Akses Gratis" : "Beli Sekarang"}
</button>
</div>
</div>
);
};
export default CourseCard;
Test setup sudah siap
Coba jalanin command ini buat memastikan setup berhasil:
npm run test
Vitest akan start dan menampilkan pesan seperti ini:

Ini normal dan bagus! Pesan ini artinya:
- ✅ Vitest berhasil dijalankan tanpa error
- ✅ Konfigurasi sudah benar
- ✅ Environment testing sudah ready
- ❗ Belum ada file test yang ditemukan (memang belum kita buat)
Apa yang bisa kamu lakukan sekarang:
- Tekan "q" buat quit dari Vitest watch mode
- Tekan "p" buat ubah pattern pencarian test files (optional)
- Biarkan running dan lanjut buat test file pertama
Test file patterns yang dikenali Vitest:
.test.tsdan.test.tsx.spec.tsdan.spec.tsx- Files di folder
__tests__/
Di tutorial selanjutnya, kita akan mulai nulis test pertama buat CourseCard component yang baru saja kita buat dengan TypeScript!
Konsep Render dan Query
Sebelum kita terjun langsung nulis test, penting banget buat paham konsep dasar testing React components. Di dunia testing, ada dua konsep utama yang harus kamu kuasai: render dan query.
Apa itu Render dalam Testing?
Render dalam konteks testing adalah proses memunculkan komponen React kita ke dalam virtual DOM yang bisa kita akses dan manipulasi. Bayangin aja kayak kamu lagi buka halaman web, tapi instead of di browser sungguhan, ini terjadi di memory komputer buat keperluan testing.
React Testing Library punya function render() yang tugasnya bikin komponen kita "hidup" dalam test environment. Function ini bakal return object yang berisi berbagai method buat berinteraksi dengan komponen tersebut.
import { render } from '@testing-library/react'
import CourseCard from './CourseCard'
// Contoh basic render
const { getByText } = render(<CourseCard title="Belajar React" />)
Memahami Query Methods
Setelah komponen di-render, kita butuh cara buat cari element-element di dalamnya. Di sinilah query methods berperan. Ada tiga jenis query yang perlu kamu tahu:
getBy* - Cari element dan langsung throw error kalau tidak ketemu. Gunakan ini kalau kamu yakin element pasti ada.
queryBy* - Cari element tapi return null kalau tidak ketemu. Berguna buat ngecek element yang mungkin tidak ada.
findBy* - Async version dari getBy. Tunggu element muncul dalam waktu tertentu sebelum throw error.
Setiap jenis query punya variant yang berbeda-beda:
getByText()- Cari berdasarkan text contentgetByRole()- Cari berdasarkan accessibility rolegetByTestId()- Cari berdasarkan data-testid attributegetByDisplayValue()- Cari berdasarkan value yang ditampilkan
Best Practice Query Selection
Urutan prioritas pemilihan query sesuai dengan filosofi Testing Library:
- getByRole() - Paling direkomendasikan karena simulate cara screen reader akses content
- getByText() - Bagus buat element yang punya text content
- getByTestId() - Last resort kalau cara lain tidak memungkinkan
Hindari query berdasarkan class name atau internal implementation karena ini bikin test fragile dan mudah rusak waktu refactoring.
Menulis Test Pertama
Sekarang saatnya nulis test pertama buat CourseCard component yang udah kita buat sebelumnya. Kita akan mulai dari test yang paling simpel dulu.
Persiapan file component
Pertama, buat file CSS di src/components/CourseCard/CourseCard.css:
.course-card {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.course-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.course-thumbnail {
position: relative;
height: 200px;
overflow: hidden;
}
.course-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.difficulty {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: white;
}
.difficulty.beginner {
background-color: #10b981;
}
.difficulty.intermediate {
background-color: #f59e0b;
}
.difficulty.advanced {
background-color: #ef4444;
}
.course-content {
padding: 16px;
}
.course-category {
font-size: 12px;
color: #6b7280;
margin-bottom: 8px;
}
.course-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1f2937;
}
.course-instructor {
font-size: 14px;
color: #6b7280;
margin: 0 0 8px 0;
}
.course-rating {
font-size: 14px;
margin-bottom: 12px;
}
.course-price {
font-size: 20px;
font-weight: 700;
color: #059669;
margin-bottom: 16px;
}
.enroll-button {
width: 100%;
padding: 12px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.enroll-button:hover {
background-color: #2563eb;
}
Setup Test File
Buat file src/components/CourseCard/CourseCard.test.tsx:
import { render, screen } from '@testing-library/react'
import CourseCard from './CourseCard'
import { CourseCardProps } from './CourseCard.types'
// Sample data buat testing
const mockCourseData: CourseCardProps = {
title: 'Mastering React Hooks BuildWithAngga',
instructor: 'Angga Risky',
price: 299000,
thumbnail: '<https://images.unsplash.com/photo-1633356122544-f134324a6cee>',
difficulty: 'Intermediate'
}
Test Structure Dasar
Setiap test file biasanya punya struktur kayak gini:
describe('CourseCard Component', () => {
it('should render course title correctly', () => {
// Arrange - Setup data dan kondisi test
render(<CourseCard {...mockCourseData} />)
// Act - Lakukan aksi (dalam case ini tidak ada)
// Assert - Verify hasil yang diharapkan
expect(screen.getByText('Mastering React Hooks BuildWithAngga')).toBeInTheDocument()
})
})
Pattern AAA (Arrange, Act, Assert)
Pattern ini adalah standar industri buat struktur test yang baik:
Arrange - Siapkan data, mock, dan kondisi yang dibutuhkan test Act - Lakukan aksi atau trigger event yang mau dites Assert - Verify bahwa hasil sesuai ekspektasi
Tidak semua test butuh ketiga tahap ini. Test rendering biasanya cuma butuh Arrange dan Assert.
Test Pertama yang Lebih Lengkap
Mari kita tulis beberapa test dasar buat CourseCard:
describe('CourseCard Component', () => {
beforeEach(() => {
// Setup yang dijalanin sebelum setiap test
})
it('should render course information correctly', () => {
render(<CourseCard {...mockCourseData} />)
expect(screen.getByText('Mastering React Hooks BuildWithAngga')).toBeInTheDocument()
expect(screen.getByText('by Angga Risky')).toBeInTheDocument()
expect(screen.getByText('Rp 299.000')).toBeInTheDocument()
expect(screen.getByText('Intermediate')).toBeInTheDocument()
})
it('should display course thumbnail with correct alt text', () => {
render(<CourseCard {...mockCourseData} />)
const thumbnail = screen.getByAltText('Mastering React Hooks BuildWithAngga')
expect(thumbnail).toBeInTheDocument()
expect(thumbnail).toHaveAttribute('src', mockCourseData.thumbnail)
})
it('should have proper test id for course card', () => {
render(<CourseCard {...mockCourseData} />)
expect(screen.getByTestId('course-card')).toBeInTheDocument()
})
})
Menjalankan Test Pertama
Sekarang coba jalanin test yang udah kita buat:
npm run test
Kalau semua berjalan lancar, kamu bakal liat output kayak gini:

Assertion Methods Dasar
Assertion adalah jantung dari testing. Ini adalah cara kita verify bahwa hasil yang didapat sesuai dengan ekspektasi. Jest dan jest-dom memberikan banyak assertion methods yang berguna.
Assertions untuk DOM Elements
toBeInTheDocument() - Ngecek apakah element ada di DOM
expect(screen.getByText('Belajar React')).toBeInTheDocument()
toHaveTextContent() - Ngecek text content dari element
expect(screen.getByRole('heading')).toHaveTextContent('Mastering React')
toHaveAttribute() - Ngecek attribute element
expect(screen.getByRole('img')).toHaveAttribute('src', 'image.jpg')
expect(screen.getByRole('button')).toHaveAttribute('disabled')
toHaveClass() - Ngecek CSS class
expect(screen.getByTestId('difficulty')).toHaveClass('intermediate')
Assertions untuk Visibility
toBeVisible() - Ngecek apakah element terlihat oleh user
expect(screen.getByText('Price')).toBeVisible()
toBeHidden() - Kebalikan dari toBeVisible
expect(screen.queryByText('Hidden content')).toBeHidden()
Assertions untuk Form Elements
toHaveValue() - Ngecek value dari input field
expect(screen.getByDisplayValue('[email protected]')).toHaveValue('[email protected]')
toBeChecked() - Ngecek checkbox atau radio button
expect(screen.getByRole('checkbox')).toBeChecked()
toBeDisabled() dan toBeEnabled() - Ngecek status enable/disable
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByRole('textbox')).toBeEnabled()
Contoh Penggunaan dalam CourseCard Test
describe('CourseCard Assertions', () => {
it('should have correct styling classes', () => {
render(<CourseCard {...mockCourseData} />)
const difficultyBadge = screen.getByText('Intermediate')
expect(difficultyBadge).toHaveClass('difficulty')
expect(difficultyBadge).toHaveClass('intermediate')
})
it('should have accessible button', () => {
const mockOnEnroll = vi.fn()
render(<CourseCard {...mockCourseData} onEnroll={mockOnEnroll} />)
const enrollButton = screen.getByRole('button')
expect(enrollButton).toBeInTheDocument()
expect(enrollButton).toBeEnabled()
expect(enrollButton).toHaveTextContent('Beli Sekarang')
})
})

Testing Props dan Rendering
Testing props adalah bagian penting dari unit testing karena props adalah interface utama komponen kita dengan dunia luar. Kita perlu mastiin komponen handle berbagai kombinasi props dengan benar.
Langkah 1: Testing required props
Pertama, test bahwa komponen bisa handle semua required props:
describe('CourseCard Props Testing', () => {
it('should render with minimum required props', () => {
const minimalProps: CourseCardProps = {
title: 'Basic Course',
instructor: 'John Doe',
price: 0,
thumbnail: 'test.jpg',
difficulty: 'Beginner'
}
render(<CourseCard {...minimalProps} />)
expect(screen.getByText('Basic Course')).toBeInTheDocument()
expect(screen.getByText('by John Doe')).toBeInTheDocument()
expect(screen.getByText('Gratis')).toBeInTheDocument()
})
})

Langkah 2: Testing optional props
Test bagaimana komponen berfungsi dengan optional props:
it('should render with optional props', () => {
const propsWithOptionals: CourseCardProps = {
...mockCourseData,
category: 'Frontend Development',
rating: 4.8,
onEnroll: vi.fn()
}
render(<CourseCard {...propsWithOptionals} />)
expect(screen.getByText('Frontend Development')).toBeInTheDocument()
expect(screen.getByText('⭐ 4.8')).toBeInTheDocument()
})

Langkah 3: Testing berbagai nilai props
Test berbagai nilai props buat mastiin komponen handle edge cases:
describe('CourseCard Different Prop Values', () => {
it('should display "Gratis" for free courses', () => {
const freeCourse = { ...mockCourseData, price: 0 }
render(<CourseCard {...freeCourse} />)
expect(screen.getByText('Gratis')).toBeInTheDocument()
expect(screen.getByText('Akses Gratis')).toBeInTheDocument()
})
it('should format price correctly for paid courses', () => {
const paidCourse = { ...mockCourseData, price: 1500000 }
render(<CourseCard {...paidCourse} />)
expect(screen.getByText('Rp 1.500.000')).toBeInTheDocument()
expect(screen.getByText('Beli Sekarang')).toBeInTheDocument()
})
it('should handle different difficulty levels', () => {
const difficulties: CourseCardProps['difficulty'][] = ['Beginner', 'Intermediate', 'Advanced']
difficulties.forEach(difficulty => {
const courseWithDifficulty = { ...mockCourseData, difficulty }
const { rerender } = render(<CourseCard {...courseWithDifficulty} />)
expect(screen.getByText(difficulty)).toBeInTheDocument()
expect(screen.getByText(difficulty)).toHaveClass(difficulty.toLowerCase())
rerender(<div></div>) // Clear component before next iteration
})
})
})

Langkah 4: Testing conditional rendering
Test bagaimana komponen render berbeda berdasarkan props:
it('should conditionally render rating when provided', () => {
// Test without rating
render(<CourseCard {...mockCourseData} />)
expect(screen.queryByText(/⭐/)).not.toBeInTheDocument()
// Test with rating
const { rerender } = render(<CourseCard {...mockCourseData} />)
rerender(<CourseCard {...mockCourseData} rating={4.5} />)
expect(screen.getByText('⭐ 4.5')).toBeInTheDocument()
})
it('should handle missing optional category', () => {
const courseWithoutCategory = {
...mockCourseData,
category: undefined
}
render(<CourseCard {...courseWithoutCategory} />)
expect(screen.getByText('Programming')).toBeInTheDocument() // default value
})

Langkah 5: Testing edge cases dengan TypeScript
Salah satu keuntungan pakai TypeScript adalah props validation otomatis. Tapi kita tetep bisa test edge cases:
it('should handle extremely long titles gracefully', () => {
const longTitle = 'A'.repeat(100) + ' BuildWithAngga Advanced Course'
const courseWithLongTitle = { ...mockCourseData, title: longTitle }
render(<CourseCard {...courseWithLongTitle} />)
expect(screen.getByText(longTitle)).toBeInTheDocument()
})
it('should handle special characters in instructor name', () => {
const specialInstructor = 'Dr. François O\\'Connor-Smith Jr.'
const courseWithSpecialInstructor = { ...mockCourseData, instructor: specialInstructor }
render(<CourseCard {...courseWithSpecialInstructor} />)
expect(screen.getByText(`by ${specialInstructor}`)).toBeInTheDocument()
})

Bonus: Reusable test utilities
Buat test yang lebih maintainable, kita bisa bikin utility functions:
// Test utility function
const renderCourseCard = (props: Partial<CourseCardProps> = {}) => {
const defaultProps = { ...mockCourseData, ...props }
return render(<CourseCard {...defaultProps} />)
}
// Penggunaan dalam test
it('should use custom utility function', () => {
renderCourseCard({ price: 0 })
expect(screen.getByText('Gratis')).toBeInTheDocument()
})

Dengan foundation testing yang kuat ini, kamu udah siap buat lanjut ke topik testing yang lebih advanced kayak user interactions dan mocking. Yang penting, selalu ingat prinsip testing: test behavior, bukan implementation!
Testing Button Clicks
Setelah kita paham cara test rendering komponen, sekarang saatnya belajar test interaksi pengguna. Button clicks adalah salah satu interaksi paling dasar yang perlu kita test dengan baik.
Mengapa testing button clicks penting?
Testing button clicks bukan cuma mastiin tombol bisa diklik, tapi juga memverifikasi bahwa aksi yang diharapkan benar-benar terjadi. Dalam aplikasi BuildWithAngga, misalnya tombol "Beli Sekarang" harus bisa trigger fungsi pembelian, atau tombol "Tambah ke Wishlist" harus bisa simpan course ke daftar favorit.
Setup komponen dengan button interactions
Mari kita tambahkan beberapa interaksi ke CourseCard component. Update CourseCard.tsx dengan fungsi-fungsi berikut:
import React, { useState } from 'react'
import { CourseCardProps } from './CourseCard.types'
import './CourseCard.css'
const CourseCard: React.FC<CourseCardProps> = ({
title,
instructor,
price,
thumbnail,
difficulty,
category = 'Programming',
rating = 0,
onEnroll,
onAddToWishlist
}) => {
const [isWishlisted, setIsWishlisted] = useState(false)
const [enrollmentCount, setEnrollmentCount] = useState(0)
const handleEnrollClick = () => {
if (onEnroll) {
onEnroll()
setEnrollmentCount(prev => prev + 1)
}
}
const handleWishlistClick = () => {
setIsWishlisted(prev => !prev)
if (onAddToWishlist) {
onAddToWishlist(!isWishlisted)
}
}
return (
<div className="course-card" data-testid="course-card">
<div className="course-thumbnail">
<img src={thumbnail} alt={title} />
<span className={`difficulty ${difficulty.toLowerCase()}`}>
{difficulty}
</span>
<button
className={`wishlist-btn ${isWishlisted ? 'active' : ''}`}
onClick={handleWishlistClick}
data-testid="wishlist-button"
aria-label={isWishlisted ? 'Hapus dari wishlist' : 'Tambah ke wishlist'}
>
{isWishlisted ? '❤️' : '🤍'}
</button>
</div>
<div className="course-content">
<div className="course-category">{category}</div>
<h3 className="course-title">{title}</h3>
<p className="course-instructor">by {instructor}</p>
{rating > 0 && (
<div className="course-rating">⭐ {rating.toFixed(1)}</div>
)}
<div className="course-price">
{price === 0 ? 'Gratis' : `Rp ${price.toLocaleString()}`}
</div>
{enrollmentCount > 0 && (
<div className="enrollment-feedback" data-testid="enrollment-count">
Tombol diklik {enrollmentCount} kali
</div>
)}
<button
className="enroll-button"
onClick={handleEnrollClick}
data-testid="enroll-button"
>
{price === 0 ? 'Akses Gratis' : 'Beli Sekarang'}
</button>
</div>
</div>
)
}
export default CourseCard
Update types untuk support interactions
Update CourseCard.types.ts buat nambah callback functions:
export interface CourseCardProps {
title: string
instructor: string
price: number
thumbnail: string
difficulty: 'Beginner' | 'Intermediate' | 'Advanced'
category?: string
rating?: number
onEnroll?: () => void
onAddToWishlist?: (isWishlisted: boolean) => void
}
Testing basic button click
Sekarang mari kita test apakah button bisa diklik dengan benar:
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import CourseCard from './CourseCard'
import { CourseCardProps } from './CourseCard.types'
const mockCourseData: CourseCardProps = {
title: 'Mastering React Hooks BuildWithAngga',
instructor: 'Angga Risky',
price: 299000,
thumbnail: '<https://images.unsplash.com/photo-1633356122544-f134324a6cee>',
difficulty: 'Intermediate'
}
describe('CourseCard Button Interactions', () => {
it('should call onEnroll when enroll button is clicked', async () => {
const user = userEvent.setup()
const mockOnEnroll = vi.fn()
render(<CourseCard {...mockCourseData} onEnroll={mockOnEnroll} />)
const enrollButton = screen.getByTestId('enroll-button')
await user.click(enrollButton)
expect(mockOnEnroll).toHaveBeenCalledTimes(1)
})
it('should update enrollment count when button clicked multiple times', async () => {
const user = userEvent.setup()
const mockOnEnroll = vi.fn()
render(<CourseCard {...mockCourseData} onEnroll={mockOnEnroll} />)
const enrollButton = screen.getByTestId('enroll-button')
await user.click(enrollButton)
expect(screen.getByTestId('enrollment-count')).toHaveTextContent('Tombol diklik 1 kali')
await user.click(enrollButton)
expect(screen.getByTestId('enrollment-count')).toHaveTextContent('Tombol diklik 2 kali')
expect(mockOnEnroll).toHaveBeenCalledTimes(2)
})
})

Testing button state changes
Kita juga perlu test perubahan state yang terjadi setelah button diklik:
describe('CourseCard Wishlist Interactions', () => {
it('should toggle wishlist state when wishlist button clicked', async () => {
const user = userEvent.setup()
const mockOnAddToWishlist = vi.fn()
render(<CourseCard {...mockCourseData} onAddToWishlist={mockOnAddToWishlist} />)
const wishlistButton = screen.getByTestId('wishlist-button')
// Initial state - not wishlisted
expect(wishlistButton).toHaveTextContent('🤍')
expect(wishlistButton).toHaveAttribute('aria-label', 'Tambah ke wishlist')
// Click to add to wishlist
await user.click(wishlistButton)
expect(wishlistButton).toHaveTextContent('❤️')
expect(wishlistButton).toHaveAttribute('aria-label', 'Hapus dari wishlist')
expect(mockOnAddToWishlist).toHaveBeenCalledWith(true)
// Click again to remove from wishlist
await user.click(wishlistButton)
expect(wishlistButton).toHaveTextContent('🤍')
expect(wishlistButton).toHaveAttribute('aria-label', 'Tambah ke wishlist')
expect(mockOnAddToWishlist).toHaveBeenCalledWith(false)
})
})

Testing Form Inputs
Form testing adalah salah satu aspek terpenting dalam testing React components. Di BuildWithAngga, form biasanya dipake buat login, registrasi, atau bahkan form pencarian course.
Membuat komponen form sederhana
Mari kita buat komponen CourseSearchForm buat testing form interactions. Buat file src/components/CourseSearchForm/CourseSearchForm.types.ts:
export interface CourseSearchFormProps {
onSearch: (searchData: SearchFormData) => void
onReset?: () => void
isLoading?: boolean
}
export interface SearchFormData {
keyword: string
category: string
difficulty: 'All' | 'Beginner' | 'Intermediate' | 'Advanced'
priceRange: 'All' | 'Free' | 'Paid'
}
Buat komponen di src/components/CourseSearchForm/CourseSearchForm.tsx:
import React, { useState } from 'react'
import { CourseSearchFormProps, SearchFormData } from './CourseSearchForm.types'
const CourseSearchForm: React.FC<CourseSearchFormProps> = ({
onSearch,
onReset,
isLoading = false
}) => {
const [formData, setFormData] = useState<SearchFormData>({
keyword: '',
category: '',
difficulty: 'All',
priceRange: 'All'
})
const [errors, setErrors] = useState<Record<string, string>>({})
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}
if (formData.keyword.trim().length < 3) {
newErrors.keyword = 'Keyword minimal 3 karakter'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
return
}
onSearch(formData)
}
const handleReset = () => {
setFormData({
keyword: '',
category: '',
difficulty: 'All',
priceRange: 'All'
})
setErrors({})
if (onReset) {
onReset()
}
}
const handleInputChange = (field: keyof SearchFormData, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
}
}
return (
<form onSubmit={handleSubmit} data-testid="course-search-form">
<div className="form-group">
<label htmlFor="keyword">Cari Course</label>
<input
id="keyword"
type="text"
value={formData.keyword}
onChange={(e) => handleInputChange('keyword', e.target.value)}
placeholder="Masukkan kata kunci..."
data-testid="keyword-input"
aria-describedby={errors.keyword ? 'keyword-error' : undefined}
/>
{errors.keyword && (
<span id="keyword-error" className="error-message" data-testid="keyword-error">
{errors.keyword}
</span>
)}
</div>
<div className="form-group">
<label htmlFor="category">Kategori</label>
<input
id="category"
type="text"
value={formData.category}
onChange={(e) => handleInputChange('category', e.target.value)}
placeholder="Frontend, Backend, Mobile..."
data-testid="category-input"
/>
</div>
<div className="form-group">
<label htmlFor="difficulty">Tingkat Kesulitan</label>
<select
id="difficulty"
value={formData.difficulty}
onChange={(e) => handleInputChange('difficulty', e.target.value as SearchFormData['difficulty'])}
data-testid="difficulty-select"
>
<option value="All">Semua Level</option>
<option value="Beginner">Pemula</option>
<option value="Intermediate">Menengah</option>
<option value="Advanced">Lanjutan</option>
</select>
</div>
<div className="form-group">
<fieldset>
<legend>Rentang Harga</legend>
<div className="radio-group">
<label>
<input
type="radio"
name="priceRange"
value="All"
checked={formData.priceRange === 'All'}
onChange={(e) => handleInputChange('priceRange', e.target.value as SearchFormData['priceRange'])}
data-testid="price-all"
/>
Semua Harga
</label>
<label>
<input
type="radio"
name="priceRange"
value="Free"
checked={formData.priceRange === 'Free'}
onChange={(e) => handleInputChange('priceRange', e.target.value as SearchFormData['priceRange'])}
data-testid="price-free"
/>
Gratis
</label>
<label>
<input
type="radio"
name="priceRange"
value="Paid"
checked={formData.priceRange === 'Paid'}
onChange={(e) => handleInputChange('priceRange', e.target.value as SearchFormData['priceRange'])}
data-testid="price-paid"
/>
Berbayar
</label>
</div>
</fieldset>
</div>
<div className="form-actions">
<button
type="submit"
disabled={isLoading}
data-testid="search-button"
>
{isLoading ? 'Mencari...' : 'Cari Course'}
</button>
<button
type="button"
onClick={handleReset}
disabled={isLoading}
data-testid="reset-button"
>
Reset
</button>
</div>
</form>
)
}
export default CourseSearchForm
Testing form input interactions
Sekarang mari kita test berbagai interaksi form:
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import CourseSearchForm from './CourseSearchForm'
describe('CourseSearchForm Input Testing', () => {
const mockOnSearch = vi.fn()
const mockOnReset = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should update input values when user types', async () => {
const user = userEvent.setup()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const keywordInput = screen.getByTestId('keyword-input')
const categoryInput = screen.getByTestId('category-input')
await user.type(keywordInput, 'React Hooks')
expect(keywordInput).toHaveValue('React Hooks')
await user.type(categoryInput, 'Frontend')
expect(categoryInput).toHaveValue('Frontend')
})
it('should update select value when option changed', async () => {
const user = userEvent.setup()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const difficultySelect = screen.getByTestId('difficulty-select')
await user.selectOptions(difficultySelect, 'Intermediate')
expect(difficultySelect).toHaveValue('Intermediate')
})
it('should update radio button selection', async () => {
const user = userEvent.setup()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const freeRadio = screen.getByTestId('price-free')
const paidRadio = screen.getByTestId('price-paid')
const allRadio = screen.getByTestId('price-all')
// Initially "All" should be selected
expect(allRadio).toBeChecked()
expect(freeRadio).not.toBeChecked()
expect(paidRadio).not.toBeChecked()
// Select "Free"
await user.click(freeRadio)
expect(freeRadio).toBeChecked()
expect(allRadio).not.toBeChecked()
expect(paidRadio).not.toBeChecked()
// Select "Paid"
await user.click(paidRadio)
expect(paidRadio).toBeChecked()
expect(freeRadio).not.toBeChecked()
expect(allRadio).not.toBeChecked()
})
})

Testing form validation
Testing validation adalah crucial buat mastiin form gak submit data yang invalid:
describe('CourseSearchForm Validation Testing', () => {
it('should show error when keyword is too short', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const keywordInput = screen.getByTestId('keyword-input')
const searchButton = screen.getByTestId('search-button')
// Type short keyword
await user.type(keywordInput, 'Re')
await user.click(searchButton)
// Should show error and not call onSearch
expect(screen.getByTestId('keyword-error')).toHaveTextContent('Keyword minimal 3 karakter')
expect(mockOnSearch).not.toHaveBeenCalled()
})
it('should clear error when user starts typing valid input', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const keywordInput = screen.getByTestId('keyword-input')
const searchButton = screen.getByTestId('search-button')
// Trigger error first
await user.type(keywordInput, 'Re')
await user.click(searchButton)
expect(screen.getByTestId('keyword-error')).toBeInTheDocument()
// Type more characters to fix the error
await user.type(keywordInput, 'act Hooks')
expect(screen.queryByTestId('keyword-error')).not.toBeInTheDocument()
})
})

Testing Events dengan User-Event Library
User-event library adalah cara terbaik buat simulate user interactions yang realistis. Library ini lebih advance dibanding fireEvent karena mensimulasi sequence events yang benar-benar terjadi waktu user berinteraksi dengan aplikasi.
Kenapa pakai user-event?
User-event library simulate user behavior yang lebih realistis. Misalnya, waktu user klik tombol, gak cuma event 'click' yang ke-trigger, tapi juga 'mousedown', 'mouseup', 'focus', dan lain-lain sesuai dengan behavior browser yang sesungguhnya.
Testing complex interactions
Mari kita test scenario yang lebih kompleks dengan multiple events:
describe('CourseSearchForm Complex Interactions', () => {
it('should handle complete search flow', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} />)
// Fill all form fields
await user.type(screen.getByTestId('keyword-input'), 'React BuildWithAngga')
await user.type(screen.getByTestId('category-input'), 'Frontend Development')
await user.selectOptions(screen.getByTestId('difficulty-select'), 'Intermediate')
await user.click(screen.getByTestId('price-paid'))
// Submit form
await user.click(screen.getByTestId('search-button'))
// Verify onSearch called with correct data
expect(mockOnSearch).toHaveBeenCalledWith({
keyword: 'React BuildWithAngga',
category: 'Frontend Development',
difficulty: 'Intermediate',
priceRange: 'Paid'
})
})
it('should reset form when reset button clicked', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
const mockOnReset = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} onReset={mockOnReset} />)
// Fill form with data
await user.type(screen.getByTestId('keyword-input'), 'Some keyword')
await user.type(screen.getByTestId('category-input'), 'Some category')
await user.selectOptions(screen.getByTestId('difficulty-select'), 'Advanced')
await user.click(screen.getByTestId('price-free'))
// Click reset
await user.click(screen.getByTestId('reset-button'))
// Verify form is reset
expect(screen.getByTestId('keyword-input')).toHaveValue('')
expect(screen.getByTestId('category-input')).toHaveValue('')
expect(screen.getByTestId('difficulty-select')).toHaveValue('All')
expect(screen.getByTestId('price-all')).toBeChecked()
expect(mockOnReset).toHaveBeenCalled()
})
})

Testing keyboard interactions
User-event juga bisa simulate keyboard interactions:
describe('CourseSearchForm Keyboard Interactions', () => {
it('should submit form when Enter key pressed in input', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} />)
const keywordInput = screen.getByTestId('keyword-input')
await user.type(keywordInput, 'React Testing')
await user.keyboard('{Enter}')
expect(mockOnSearch).toHaveBeenCalledWith({
keyword: 'React Testing',
category: '',
difficulty: 'All',
priceRange: 'All'
})
})
it('should navigate through form using Tab key', async () => {
const user = userEvent.setup()
render(<CourseSearchForm onSearch={vi.fn()} />)
const keywordInput = screen.getByTestId('keyword-input')
const categoryInput = screen.getByTestId('category-input')
const difficultySelect = screen.getByTestId('difficulty-select')
// Start from keyword input
keywordInput.focus()
expect(keywordInput).toHaveFocus()
// Tab to next field
await user.keyboard('{Tab}')
expect(categoryInput).toHaveFocus()
// Tab to select
await user.keyboard('{Tab}')
expect(difficultySelect).toHaveFocus()
})
})

Testing loading states
Testing component behavior dalam loading state:
describe('CourseSearchForm Loading States', () => {
it('should disable buttons when loading', () => {
render(<CourseSearchForm onSearch={vi.fn()} isLoading={true} />)
const searchButton = screen.getByTestId('search-button')
const resetButton = screen.getByTestId('reset-button')
expect(searchButton).toBeDisabled()
expect(resetButton).toBeDisabled()
expect(searchButton).toHaveTextContent('Mencari...')
})
it('should not submit form when loading', async () => {
const user = userEvent.setup()
const mockOnSearch = vi.fn()
render(<CourseSearchForm onSearch={mockOnSearch} isLoading={true} />)
// Try to submit form
await user.click(screen.getByTestId('search-button'))
// Should not call onSearch because button is disabled
expect(mockOnSearch).not.toHaveBeenCalled()
})
})

Best practices testing events
Beberapa tips penting waktu testing user interactions:
- Selalu pakai async/await dengan user-event functions
- Setup user-event di awal setiap test dengan
userEvent.setup() - Test dari perspektif user, bukan implementation details
- Mock external dependencies yang gak relevam dengan interaction testing
- Test error scenarios dan edge cases
- Verify both state changes dan function calls
Dengan menguasai testing user interactions, kamu udah bisa ngetes hampir semua aspek behavior komponen React.
Kesimpulan
Selamat! Kamu udah berhasil menyelesaikan panduan lengkap React Unit Testing dengan Vite. Dari setup awal sampai testing user interactions, semua udah kita bahas dengan detail. Tapi ingat, belajar testing itu kayak belajar naik sepeda - teori doang gak cukup, kamu butuh praktek langsung.
Kenapa butuh belajar dengan real project?
Di BuildWithAngga, kamu gak cuma belajar dari video tutorial biasa. Kamu bakal dapet kesempatan buat ngerjin project nyata yang udah dipake di industri. Misalnya, bikin aplikasi e-learning lengkap dengan fitur authentication dan dashboard admin yang butuh testing comprehensive.
Mentor berpengalaman dan akses selamanya
Mentor di BuildWithAngga adalah praktisi industri yang udah bertahun-tahun ngerjin project production. Mereka bisa kasih feedback langsung ke kode testing kamu dan sharing best practices yang susah didapet dari tutorial online biasa.
Plus, dengan akses selamanya, kamu bisa balik lagi ke materi kapan aja. Course content juga selalu diupdate sesuai perkembangan industri, jadi gak perlu khawatir ketinggalan teknologi baru.
Investment yang worthwhile
Skill React Testing yang solid bisa naikin market value kamu secara signifikan. Di job market Indonesia sekarang, developer yang bisa nulis test dengan baik masih jarang, jadi demand-nya tinggi banget. Biasanya dalam beberapa bulan kamu udah bisa dapet offer dengan salary yang lebih tinggi.
Mulai sekarang
Testing skill itu gak akan outdated dalam waktu dekat, malah makin penting. BuildWithAngga udah prepare semua yang kamu butuhkan buat jadi React developer yang well-rounded.
Remember, every expert was once a beginner. Yang bikin mereka jadi expert adalah consistency dalam belajar dan praktek. Tunggu apa lagi? Start your learning journey sekarang dan prepare yourself buat jadi developer yang dicari-cari di industri!