Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Testing with Vitest and React Testing Library in React

Testing with Vitest and React Testing Library

~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

So far we've manually checked every change in the browser – it works, but quickly becomes impractical as the project grows: with EVERY change, you'd have to re-click through ALL previous features by hand. Automated tests take over exactly that job.

Installing Vitest and React Testing Library

Vitest is a test runner built specifically for Vite projects (uses the same config, the same speed) – the direct, modern equivalent of the older Jest. React Testing Library ("RTL") is the library for testing components the way a REAL user would interact with them (clicks, typing, visible text), instead of checking internal implementation details.

npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

Extending vite.config.js with test configuration

vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom', // simulates a browser in Node.js, no real browser needed
    globals: true,        // allows describe/it/expect without importing them in every test file
    setupFiles: './src/test/setup.js',
  },
});
src/test/setup.js
import '@testing-library/jest-dom';
// extends expect() with matchers like .toBeInTheDocument(), .toHaveTextContent(), etc.

The first test: a pure function

We'll start with the SIMPLEST case: you already know bmiCategory() from the "React Native Reference" series as this pattern – a pure function (no component, no state) is easiest to test. Our project doesn't have a BMI example, but filterItemsSlowly's filter logic from SearchDemoPage (chapter 33) is structurally similar. Instead, let's test something from our actual project: the total-price calculation from CartPage, extracted as its own, testable function:

src/utils/cartTotal.js
export function calculateCartTotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
src/utils/cartTotal.test.js
import { describe, it, expect } from 'vitest';
import { calculateCartTotal } from './cartTotal';

describe('calculateCartTotal', () => {
  it('returns 0 for an empty cart', () => {
    expect(calculateCartTotal([])).toBe(0);
  });

  it('calculates the total for a single item', () => {
    const items = [{ price: 10, quantity: 2 }];
    expect(calculateCartTotal(items)).toBe(20);
  });

  it('correctly sums multiple different items', () => {
    const items = [
      { price: 10, quantity: 2 },
      { price: 5, quantity: 3 },
    ];
    expect(calculateCartTotal(items)).toBe(35);
  });
});

describe groups related tests, it (alias: test) describes ONE concrete test case in plain language, expect(...).toBe(...) is the assertion itself. Add a script to package.json: "test": "vitest", then run npm test – Vitest automatically watches files and re-runs on every change.

Testing a component: ProductCard

Component tests do NOT check internal state directly, they check VISIBLE behavior – EXACTLY how a user experiences the app. RTL's render() renders the component into a virtual DOM environment, screen finds elements inside it:

src/components/ProductCard.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProductCard from './ProductCard';

describe('ProductCard', () => {
  const defaultProps = {
    name: 'Hiking Boots',
    price: 89.99,
    imageUrl: 'https://example.com/boots.jpg',
    onSelect: vi.fn(),
    onAddToCart: vi.fn(),
  };

  it('displays the name and price', () => {
    render(<ProductCard {...defaultProps} />);
    expect(screen.getByText('Hiking Boots')).toBeInTheDocument();
    expect(screen.getByText('$89.99')).toBeInTheDocument();
  });

  it('calls onAddToCart when the cart button is clicked', async () => {
    const user = userEvent.setup();
    render(<ProductCard {...defaultProps} />);

    await user.click(screen.getByText('Add to Cart'));

    expect(defaultProps.onAddToCart).toHaveBeenCalledOnce();
    expect(defaultProps.onSelect).not.toHaveBeenCalled();
  });

  it('toggles favorite status when the heart is clicked', async () => {
    const user = userEvent.setup();
    render(<ProductCard {...defaultProps} />);

    expect(screen.getByText('♡')).toBeInTheDocument();
    await user.click(screen.getByText('♡'));
    expect(screen.getByText('♥')).toBeInTheDocument();
  });
});

The details that make this test good

  • vi.fn() creates a "mock function" – a test stand-in that gets recorded but does nothing itself. toHaveBeenCalledOnce() checks it was called EXACTLY once.
  • screen.getByText(...) searches for VISIBLE text – exactly what a real user would see, not internal variable names or CSS classes.
  • expect(defaultProps.onSelect).not.toHaveBeenCalled() in the "Add to Cart" test INDIRECTLY verifies that event.stopPropagation() from chapter 29 actually works – the EXACT bug stopPropagation() is meant to prevent would make this test fail.
  • userEvent.setup() instead of the older fireEvent simulates REAL user interaction more realistically (including intermediate steps like focusing before clicking).

Achtung: Don't test isFavorite as internal state directly (e.g. via component instance access) – RTL deliberately doesn't offer that. The test instead checks the VISIBLE outcome ( becomes ) – the test stays valid even if you later change the internal implementation (e.g. moving isFavorite into a Zustand store), as long as the visible behavior stays the same.

Tipp: React Testing Library's often-quoted guiding principle: "The more your tests resemble the way your software is used, the more confidence they can give you." Tests that check implementation details break on every refactor, even when the VISIBLE behavior stays unchanged – the exact opposite of what good tests are supposed to accomplish.