React Unit Testing dengan Vite: Tutorial Testing Components Lengkap untuk Pemula 2025

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

Terminal: Setup Project
Terminal: Setup Project

Metode 2: Interactive CLI (recommended buat pemula)

npm create vite@latest

Lalu ikuti prompt berikut:

  1. Project name: ketik buildwithangga-testing
  2. Select framework: pilih React
  3. 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.

Tampilan Awal Vite
Tampilan Awal Vite

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.

VSCode: Install Library yang Dibutuhkan
VSCode: Install Library yang Dibutuhkan

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 mode
  • test:ui - Buka Vitest UI interface di browser
  • test:run - Run tests sekali aja tanpa watch
  • test: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:

Struktur Project
Struktur Project

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:

Terminal: Setup Vites Berhasil
Terminal: Setup Vites Berhasil

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:

  1. Tekan "q" buat quit dari Vitest watch mode
  2. Tekan "p" buat ubah pattern pencarian test files (optional)
  3. Biarkan running dan lanjut buat test file pertama

Test file patterns yang dikenali Vitest:

  • .test.ts dan .test.tsx
  • .spec.ts dan .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 content
  • getByRole() - Cari berdasarkan accessibility role
  • getByTestId() - Cari berdasarkan data-testid attribute
  • getByDisplayValue() - Cari berdasarkan value yang ditampilkan

Best Practice Query Selection

Urutan prioritas pemilihan query sesuai dengan filosofi Testing Library:

  1. getByRole() - Paling direkomendasikan karena simulate cara screen reader akses content
  2. getByText() - Bagus buat element yang punya text content
  3. 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:

Terminal: Test Berjalan dengan Baik
Terminal: Test Berjalan dengan Baik

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')
  })
})

Terminal: Test Assertions
Terminal: Test Assertions

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()
  })
})

Terminal: Test Props dan Rendering
Terminal: Test Props dan Rendering

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()
})

Terminal: Test Optional Props
Terminal: Test Optional Props

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
    })
  })
})

Terminal: Test Berbagai Nilai Props
Terminal: Test Berbagai Nilai Props

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
})

Terminal: Test Conditional Rendering
Terminal: Test Conditional Rendering

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()
})

Terminal: Test Edge Cases dengan TypeScript
Terminal: Test Edge Cases dengan TypeScript

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()
})

Terminal: Reuseable Test Utilities
Terminal: Reuseable Test Utilities

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)
  })
})

Terminal: Test Basic Button Click
Terminal: Test Basic Button Click

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)
  })
})

Terminal: Test Button State Changes
Terminal: Test Button State Changes

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()
  })
})

Terminal: Test Form Input Interaction
Terminal: Test Form Input Interaction

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()
  })
})

Terminal: Test Form Validation
Terminal: Test Form Validation

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()
  })
})

Terminal: Test Complex Interactions
Terminal: Test Complex Interactions

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()
  })
})

Terminal: Test Keyboard Interactions
Terminal: Test Keyboard Interactions

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()
  })
})

Terminal: Test Loading State
Terminal: Test Loading State

Best practices testing events

Beberapa tips penting waktu testing user interactions:

  1. Selalu pakai async/await dengan user-event functions
  2. Setup user-event di awal setiap test dengan userEvent.setup()
  3. Test dari perspektif user, bukan implementation details
  4. Mock external dependencies yang gak relevam dengan interaction testing
  5. Test error scenarios dan edge cases
  6. 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!