Svenska

Bemästra React Testing Library (RTL) med denna kompletta guide. Lär dig skriva effektiva, underhållbara och användarcentrerade tester för dina React-applikationer.

React Testing Library: En omfattande guide

I dagens snabbrörliga landskap för webbutveckling är det avgörande att säkerställa kvaliteten och tillförlitligheten hos dina React-applikationer. React Testing Library (RTL) har vuxit fram som en populär och effektiv lösning för att skriva tester som fokuserar på användarperspektivet. Denna guide ger en komplett översikt över RTL, och täcker allt från grundläggande koncept till avancerade tekniker, för att ge dig kraften att bygga robusta och underhållbara React-applikationer.

Varför välja React Testing Library?

Traditionella testmetoder förlitar sig ofta på implementeringsdetaljer, vilket gör testerna sköra och benägna att gå sönder vid mindre kodändringar. RTL, å andra sidan, uppmuntrar dig att testa dina komponenter som en användare skulle interagera med dem, med fokus på vad användaren ser och upplever. Detta tillvägagångssätt erbjuder flera viktiga fördelar:

Konfigurera din testmiljö

Innan du kan börja använda RTL måste du konfigurera din testmiljö. Detta innebär vanligtvis att installera nödvändiga beroenden och konfigurera ditt testramverk.

Förutsättningar

Installation

Installera följande paket med npm eller yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Eller, med yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Förklaring av paket:

Konfiguration

Skapa en fil med namnet `babel.config.js` i roten av ditt projekt med följande innehåll:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Uppdatera din `package.json`-fil för att inkludera ett testskript:

{
  "scripts": {
    "test": "jest"
  }
}

Skapa en `jest.config.js`-fil i roten av ditt projekt för att konfigurera Jest. En minimal konfiguration kan se ut så här:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Skapa en `src/setupTests.js`-fil med följande innehåll. Detta säkerställer att Jest DOM-matchers är tillgängliga i alla dina tester:

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

Skriva ditt första test

Låt oss börja med ett enkelt exempel. Anta att du har en React-komponent som visar ett hälsningsmeddelande:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

Låt oss nu skriva ett test för den här komponenten:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renders a greeting message', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/Hello, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

Förklaring:

För att köra testet, kör följande kommando i din terminal:

npm test

Om allt är korrekt konfigurerat bör testet passera.

Vanliga RTL-sökfrågor

RTL tillhandahåller olika sökmetoder för att hitta element i DOM. Dessa sökfrågor är utformade för att efterlikna hur användare interagerar med din applikation.

`getByRole`

Denna sökfråga hittar ett element baserat på dess ARIA-roll. Det är god praxis att använda `getByRole` när det är möjligt, eftersom det främjar tillgänglighet och säkerställer att dina tester är motståndskraftiga mot ändringar i den underliggande DOM-strukturen.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Denna sökfråga hittar ett element via texten i dess tillhörande etikett. Det är användbart för att testa formulärelement.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Denna sökfråga hittar ett element via dess platshållartext.

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Denna sökfråga hittar ett bildelement via dess alt-text. Det är viktigt att tillhandahålla meningsfull alt-text för alla bilder för att säkerställa tillgänglighet.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Denna sökfråga hittar ett element via dess title-attribut.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Denna sökfråga hittar ett element via dess visningsvärde. Detta är användbart för att testa formulärfält med förifyllda värden.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

`getAllBy*`-sökfrågor

Utöver `getBy*`-sökfrågorna tillhandahåller RTL också `getAllBy*`-sökfrågor, som returnerar en array av matchande element. Dessa är användbara när du behöver försäkra dig om att flera element med samma egenskaper finns i DOM.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

`queryBy*`-sökfrågor

`queryBy*`-sökfrågorna liknar `getBy*`-sökfrågorna, men de returnerar `null` om inget matchande element hittas, istället för att kasta ett fel. Detta är användbart när du vill försäkra dig om att ett element *inte* finns i DOM.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

`findBy*`-sökfrågor

`findBy*`-sökfrågorna är asynkrona versioner av `getBy*`-sökfrågorna. De returnerar ett Promise som uppfylls när det matchande elementet hittas. Dessa är användbara för att testa asynkrona operationer, som att hämta data från ett API.

// Simulating an asynchronous data fetch
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

Simulera användarinteraktioner

RTL tillhandahåller API:erna `fireEvent` och `userEvent` för att simulera användarinteraktioner, som att klicka på knappar, skriva i inmatningsfält och skicka formulär.

`fireEvent`

`fireEvent` låter dig programmatiskt utlösa DOM-händelser. Det är ett lägre nivå-API som ger dig finkornig kontroll över de händelser som utlöses.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('simulates a button click', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` är ett högre nivå-API som simulerar användarinteraktioner mer realistiskt. Det hanterar detaljer som fokushantering och händelseordning, vilket gör dina tester mer robusta och mindre sköra.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simulates typing in an input field', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

Testa asynkron kod

Många React-applikationer involverar asynkrona operationer, som att hämta data från ett API. RTL tillhandahåller flera verktyg för att testa asynkron kod.

`waitFor`

`waitFor` låter dig vänta på att ett villkor ska bli sant innan du gör en assertion. Det är användbart för att testa asynkrona operationer som tar lite tid att slutföra.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('waits for data to load', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

`findBy*`-sökfrågor

Som nämnts tidigare är `findBy*`-sökfrågorna asynkrona och returnerar ett Promise som uppfylls när det matchande elementet hittas. Dessa är användbara för att testa asynkrona operationer som resulterar i ändringar i DOM.

Testa Hooks

React Hooks är återanvändbara funktioner som kapslar in stateful logik. RTL tillhandahåller verktyget `renderHook` från `@testing-library/react-hooks` (som är föråldrat till förmån för `@testing-library/react` direkt från och med v17) för att testa anpassade Hooks isolerat.

// src/hooks/useCounter.js
import { useState } from 'react';

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

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('increments the counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Förklaring:

Avancerade testtekniker

När du har bemästrat grunderna i RTL kan du utforska mer avancerade testtekniker för att förbättra kvaliteten och underhållbarheten på dina tester.

Mocka moduler

Ibland kan du behöva mocka externa moduler eller beroenden för att isolera dina komponenter och kontrollera deras beteende under testning. Jest tillhandahåller ett kraftfullt mocknings-API för detta ändamål.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('fetches data from the API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Förklaring:

Context Providers

Om din komponent är beroende av en Context Provider, måste du omsluta din komponent i providern under testning. Detta säkerställer att komponenten har tillgång till kontextvärdena.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('toggles the theme', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

Förklaring:

Testa med Router

När du testar komponenter som använder React Router, måste du tillhandahålla en mockad Router-kontext. Du kan uppnå detta genom att använda `MemoryRouter`-komponenten från `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('renders a link to the about page', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Förklaring:

Bästa praxis för att skriva effektiva tester

Här är några bästa praxis att följa när du skriver tester med RTL:

Slutsats

React Testing Library är ett kraftfullt verktyg för att skriva effektiva, underhållbara och användarcentrerade tester för dina React-applikationer. Genom att följa principerna och teknikerna som beskrivs i denna guide kan du bygga robusta och pålitliga applikationer som möter dina användares behov. Kom ihåg att fokusera på att testa från användarens perspektiv, undvika att testa implementeringsdetaljer och skriva tydliga och koncisa tester. Genom att anamma RTL och tillämpa bästa praxis kan du avsevärt förbättra kvaliteten och underhållbarheten i dina React-projekt, oavsett din plats eller de specifika kraven från din globala publik.