Introductie

Nu je de basis matchers kent, gaan we echte React componenten testen met interactie: buttons, inputs, forms en events.

Wat ga je leren?

  • Button clicks simuleren
  • Input velden testen
  • Formulieren testen
  • State veranderingen testen

Belangrijk: userEvent importeren

We gebruiken userEvent om user interacties te simuleren. Dit import je zo:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

Button met Click Event

Laten we beginnen met een simpele button die clicks telt.

Het Component

Maak een bestand: src/components/LikeButton.jsx

import { useState } from 'react';

function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <div>
      <button onClick={() => setLikes(likes + 1)}>
        Like
      </button>
      <p>Likes: {likes}</p>
    </div>
  );
}

export default LikeButton;

De Test

Maak een test: src/components/LikeButton.test.jsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LikeButton from './LikeButton';

test('begint met 0 likes', () => {
  render(<LikeButton />);
  
  expect(screen.getByText('Likes: 0')).toBeInTheDocument();
});

test('verhoogt likes bij klik', async () => {
  // Setup userEvent
  const user = userEvent.setup();
  
  render(<LikeButton />);
  
  // Vind de button
  const button = screen.getByText('Like');
  
  // Klik op de button
  await user.click(button);
  
  // Check of likes verhoogd is
  expect(screen.getByText('Likes: 1')).toBeInTheDocument();
});

test('kan meerdere keren klikken', async () => {
  const user = userEvent.setup();
  render(<LikeButton />);
  
  const button = screen.getByText('Like');
  
  // Klik 3 keer
  await user.click(button);
  await user.click(button);
  await user.click(button);
  
  expect(screen.getByText('Likes: 3')).toBeInTheDocument();
});

Let op:

  • const user = userEvent.setup() - Maak een user instance
  • async/await - userEvent functies zijn async!
  • await user.click(button) - Simuleer een click

Input Velden Testen

Laten we een input veld testen waar je je naam kunt typen.

Het Component

Maak een bestand: src/components/NameInput.jsx

import { useState } from 'react';

function NameInput() {
  const [name, setName] = useState('');

  return (
    <div>
      <input
        type="text"
        placeholder="Jouw naam"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      {name && <p>Hallo, {name}!</p>}
    </div>
  );
}

export default NameInput;

De Test

Maak een test: src/components/NameInput.test.jsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import NameInput from './NameInput';

test('begint met lege input', () => {
  render(<NameInput />);
  
  const input = screen.getByPlaceholderText('Jouw naam');
  expect(input).toHaveValue('');
});

test('toont begroeting na typen', async () => {
  const user = userEvent.setup();
  render(<NameInput />);
  
  const input = screen.getByPlaceholderText('Jouw naam');
  
  // Typ in het input veld
  await user.type(input, 'Jan');
  
  // Check of input de juiste waarde heeft
  expect(input).toHaveValue('Jan');
  
  // Check of begroeting verschijnt
  expect(screen.getByText('Hallo, Jan!')).toBeInTheDocument();
});

user.type() tips:

  • await user.type(input, 'tekst') - Typ tekst in een veld
  • screen.getByPlaceholderText('...') - Vind input via placeholder
  • toHaveValue('...') - Check input waarde

Formulier Testen

Nu een volledig formulier met validatie.

Het Component

Maak een bestand: src/components/ContactForm.jsx

import { useState } from 'react';

function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setSubmitted(true);
  };

  const isValid = name.length > 0 && email.includes('@');

  return (
    <div>
      {submitted ? (
        <p>Bedankt, {name}!</p>
      ) : (
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            placeholder="Naam"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
          <input
            type="email"
            placeholder="Email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <button type="submit" disabled={!isValid}>
            Verstuur
          </button>
        </form>
      )}
    </div>
  );
}

export default ContactForm;

De Test

Maak een test: src/components/ContactForm.test.jsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ContactForm from './ContactForm';

test('button is disabled als form leeg is', () => {
  render(<ContactForm />);
  
  const button = screen.getByText('Verstuur');
  expect(button).toBeDisabled();
});

test('button is enabled als form geldig is', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);
  
  // Vul naam in
  const nameInput = screen.getByPlaceholderText('Naam');
  await user.type(nameInput, 'Jan');
  
  // Vul email in
  const emailInput = screen.getByPlaceholderText('Email');
  await user.type(emailInput, 'jan@test.com');
  
  // Check of button nu enabled is
  const button = screen.getByText('Verstuur');
  expect(button).not.toBeDisabled();
});

test('toont bedankt bericht na submit', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);
  
  // Vul form in
  await user.type(screen.getByPlaceholderText('Naam'), 'Jan');
  await user.type(screen.getByPlaceholderText('Email'), 'jan@test.com');
  
  // Submit form
  const button = screen.getByText('Verstuur');
  await user.click(button);
  
  // Check bedankt bericht
  expect(screen.getByText('Bedankt, Jan!')).toBeInTheDocument();
  
  // Check of form verdwenen is
  expect(screen.queryByPlaceholderText('Naam')).not.toBeInTheDocument();
});

queryBy vs getBy:

  • getByText() - Gooit error als element niet bestaat
  • queryByText() - Geeft null als element niet bestaat
  • Gebruik queryBy om te checken dat iets NIET bestaat!

Complete Voorbeeld: Counter App

Laten we alles combineren in een counter app met plus, min en reset buttons.

Het Component

Maak een bestand: src/components/Counter.jsx

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default Counter;

De Test Suite

Maak een test: src/components/Counter.test.jsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

describe('Counter component', () => {
  
  test('begint met 0', () => {
    render(<Counter />);
    expect(screen.getByText('Counter: 0')).toBeInTheDocument();
  });

  test('verhoogt bij plus button', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const plusButton = screen.getByText('+');
    await user.click(plusButton);
    
    expect(screen.getByText('Counter: 1')).toBeInTheDocument();
  });

  test('verlaagt bij min button', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const minButton = screen.getByText('-');
    await user.click(minButton);
    
    expect(screen.getByText('Counter: -1')).toBeInTheDocument();
  });

  test('reset naar 0', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    // Eerst verhogen naar 5
    const plusButton = screen.getByText('+');
    await user.click(plusButton);
    await user.click(plusButton);
    await user.click(plusButton);
    await user.click(plusButton);
    await user.click(plusButton);
    
    expect(screen.getByText('Counter: 5')).toBeInTheDocument();
    
    // Dan reset
    const resetButton = screen.getByText('Reset');
    await user.click(resetButton);
    
    expect(screen.getByText('Counter: 0')).toBeInTheDocument();
  });

  test('kan meerdere acties na elkaar doen', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const plusButton = screen.getByText('+');
    const minButton = screen.getByText('-');
    
    // +3
    await user.click(plusButton);
    await user.click(plusButton);
    await user.click(plusButton);
    expect(screen.getByText('Counter: 3')).toBeInTheDocument();
    
    // -1
    await user.click(minButton);
    expect(screen.getByText('Counter: 2')).toBeInTheDocument();
  });
  
});

Run de tests:

npm run test

Je zou moeten zien:

PASS  src/components/Counter.test.jsx
  Counter component
    ✓ begint met 0 (24ms)
    ✓ verhoogt bij plus button (45ms)
    ✓ verlaagt bij min button (38ms)
    ✓ reset naar 0 (67ms)
    ✓ kan meerdere acties na elkaar doen (56ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total

Goed gedaan! Je hebt nu een complete component getest met meerdere interacties en state veranderingen.

Tips & Tricks

1. Gebruik screen.debug() voor debugging

test('debug voorbeeld', () => {
  render(<Counter />);
  
  // Print de hele DOM tree naar console
  screen.debug();
  
  // Print een specifiek element
  const button = screen.getByText('+');
  screen.debug(button);
});

2. Verschillende manieren om elementen te vinden

// Via tekst
screen.getByText('Klik hier');

// Via placeholder
screen.getByPlaceholderText('Jouw naam');

// Via role (toegankelijk!)
screen.getByRole('button');
screen.getByRole('textbox');

// Via label
screen.getByLabelText('Email');

// Via test ID (laatste optie!)
screen.getByTestId('submit-button');

3. Veelgemaakte fouten

Fout 1: Vergeten await bij userEvent

// FOUT - geen await
user.click(button); // Test faalt!

// GOED
await user.click(button);

Fout 2: Vergeten async bij test

// FOUT - geen async
test('klikt op button', () => {
  await user.click(button); // Error!
});

// GOED
test('klikt op button', async () => {
  await user.click(button);
});

Fout 3: getBy gebruiken voor iets dat niet bestaat

// FOUT - gooit error als niet gevonden
expect(screen.getByText('Niet bestaand')).not.toBeInTheDocument();

// GOED - gebruik queryBy
expect(screen.queryByText('Niet bestaand')).not.toBeInTheDocument();

4. Best Practices

Test gedrag, niet implementatie

// SLECHT - test state direct
expect(component.state.count).toBe(5);

// GOED - test wat de user ziet
expect(screen.getByText('Counter: 5')).toBeInTheDocument();

Gebruik toegankelijke queries

// SLECHT - test ID
screen.getByTestId('submit-btn');

// GOED - role (zoals screenreader)
screen.getByRole('button', { name: 'Verstuur' });

Wat nu?

Je kent nu de basis van Jest testing! Je kunt:

  • Functies testen met matchers
  • React componenten renderen
  • User interacties simuleren
  • State veranderingen checken