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:
- Användarcentrerad testning: RTL främjar skrivandet av tester som återspeglar användarens perspektiv, vilket säkerställer att din applikation fungerar som förväntat ur slutanvändarens synvinkel.
- Minskad testskörhet: Genom att undvika att testa implementeringsdetaljer är det mindre troligt att RTL-tester går sönder när du refaktorerar din kod, vilket leder till mer underhållbara och robusta tester.
- Förbättrad koddesign: RTL uppmuntrar dig att skriva komponenter som är tillgängliga och lätta att använda, vilket leder till en bättre övergripande koddesign.
- Fokus på tillgänglighet: RTL gör det enklare att testa tillgängligheten hos dina komponenter, vilket säkerställer att din applikation kan användas av alla.
- Förenklad testprocess: RTL tillhandahåller ett enkelt och intuitivt API, vilket gör det lättare att skriva och underhålla tester.
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
- Node.js och npm (eller yarn): Se till att du har Node.js och npm (eller yarn) installerat på ditt system. Du kan ladda ner dem från den officiella Node.js-webbplatsen.
- React-projekt: Du bör ha ett befintligt React-projekt eller skapa ett nytt med Create React App eller ett liknande verktyg.
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:
- @testing-library/react: Kärnbiblioteket för att testa React-komponenter.
- @testing-library/jest-dom: Tillhandahåller anpassade Jest-matchers för att göra assertions om DOM-noder.
- Jest: Ett populärt JavaScript-testramverk.
- babel-jest: En Jest-transformer som använder Babel för att kompilera din kod.
- @babel/preset-env: En Babel-preset som bestämmer vilka Babel-plugins och presets som behövs för att stödja dina målmiljöer.
- @babel/preset-react: En Babel-preset för React.
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:
- `render`: Denna funktion renderar komponenten i DOM.
- `screen`: Detta objekt tillhandahåller metoder för att söka i DOM.
- `getByText`: Denna metod hittar ett element via dess textinnehåll. Flaggan `/i` gör sökningen skiftlägesokänslig.
- `expect`: Denna funktion används för att göra assertions om komponentens beteende.
- `toBeInTheDocument`: Denna matcher försäkrar att elementet finns i DOM.
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:
- `renderHook`: Denna funktion renderar Hooken och returnerar ett objekt som innehåller resultatet av Hooken.
- `act`: Denna funktion används för att omsluta all kod som orsakar tillståndsuppdateringar. Detta säkerställer att React kan bunta ihop och bearbeta uppdateringarna korrekt.
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:
- `jest.mock('../api/dataService')`: Denna rad mockar `dataService`-modulen.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Denna rad konfigurerar den mockade `fetchData`-funktionen att returnera ett Promise som uppfylls med den specificerade datan.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Denna rad försäkrar att den mockade `fetchData`-funktionen anropades en gång.
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:
- Vi omsluter `MyComponent` i `ThemeProvider` för att tillhandahålla den nödvändiga kontexten under testning.
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:
- Vi omsluter `MyComponent` i `MemoryRouter` för att tillhandahålla en mockad Router-kontext.
- Vi försäkrar att länk-elementet har det korrekta `href`-attributet.
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:
- Fokusera på användarinteraktioner: Skriv tester som simulerar hur användare interagerar med din applikation.
- Undvik att testa implementeringsdetaljer: Testa inte den interna funktionen hos dina komponenter. Fokusera istället på det observerbara beteendet.
- Skriv tydliga och koncisa tester: Gör dina tester lätta att förstå och underhålla.
- Använd meningsfulla testnamn: Välj testnamn som korrekt beskriver det beteende som testas.
- Håll tester isolerade: Undvik beroenden mellan tester. Varje test bör vara oberoende och fristående.
- Testa kantfall: Testa inte bara den glada vägen. Se till att även testa kantfall och feltillstånd.
- Skriv tester innan du kodar: Överväg att använda testdriven utveckling (TDD) för att skriva tester innan du skriver din kod.
- Följ "AAA"-mönstret: Arrange, Act, Assert. Detta mönster hjälper till att strukturera dina tester och göra dem mer läsbara.
- Håll dina tester snabba: Långsamma tester kan avskräcka utvecklare från att köra dem ofta. Optimera dina tester för hastighet genom att mocka nätverksanrop och minimera mängden DOM-manipulation.
- Använd beskrivande felmeddelanden: När assertions misslyckas bör felmeddelandena ge tillräckligt med information för att snabbt identifiera orsaken till felet.
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.