שלטו ב-React Testing Library (RTL) עם מדריך מקיף זה. למדו כיצד לכתוב בדיקות יעילות, קלות לתחזוקה וממוקדות-משתמש עבור אפליקציות הריאקט שלכם, תוך התמקדות בשיטות עבודה מומלצות ודוגמאות מהעולם האמיתי.
React Testing Library: מדריך מקיף
בנוף פיתוח הרשת המהיר של ימינו, הבטחת האיכות והאמינות של אפליקציות הריאקט שלכם היא בעלת חשיבות עליונה. React Testing Library (RTL) הפכה לפתרון פופולרי ויעיל לכתיבת בדיקות המתמקדות בפרספקטיבה של המשתמש. מדריך זה מספק סקירה מלאה של RTL, המכסה הכל החל מהמושגים הבסיסיים ועד לטכניקות מתקדמות, ומעצים אתכם לבנות אפליקציות ריאקט חזקות וקלות לתחזוקה.
למה לבחור ב-React Testing Library?
גישות בדיקה מסורתיות מסתמכות לעיתים קרובות על פרטי יישום, מה שהופך את הבדיקות לשבירות ונוטות להישבר עם שינויי קוד קלים. RTL, לעומת זאת, מעודדת אתכם לבדוק את הקומפוננטות שלכם כפי שמשתמש היה מתקשר איתן, תוך התמקדות במה שהמשתמש רואה וחווה. גישה זו מציעה מספר יתרונות מרכזיים:
- בדיקות ממוקדות-משתמש: RTL מקדמת כתיבת בדיקות המשקפות את נקודת המבט של המשתמש, ומבטיחה שהאפליקציה שלכם מתפקדת כמצופה מנקודת מבטו של משתמש הקצה.
- הפחתת שבירות הבדיקות: על ידי הימנעות מבדיקת פרטי יישום, בדיקות RTL נוטות פחות להישבר כאשר אתם מבצעים ריפקטורינג לקוד שלכם, מה שמוביל לבדיקות קלות יותר לתחזוקה וחזקות יותר.
- עיצוב קוד משופר: RTL מעודדת אתכם לכתוב קומפוננטות נגישות וקלות לשימוש, מה שמוביל לעיצוב קוד כללי טוב יותר.
- התמקדות בנגישות: RTL מקלה על בדיקת הנגישות של הקומפוננטות שלכם, ומבטיחה שהאפליקציה שלכם שמישה עבור כולם.
- תהליך בדיקה פשוט יותר: RTL מספקת API פשוט ואינטואיטיבי, המקל על כתיבה ותחזוקה של בדיקות.
הגדרת סביבת הבדיקות שלכם
לפני שתוכלו להתחיל להשתמש ב-RTL, עליכם להגדיר את סביבת הבדיקות שלכם. זה בדרך כלל כולל התקנת התלויות הנדרשות והגדרת פריימוורק הבדיקות שלכם.
דרישות קדם
- Node.js ו-npm (או yarn): ודאו ש-Node.js ו-npm (או yarn) מותקנים במערכת שלכם. ניתן להוריד אותם מהאתר הרשמי של Node.js.
- פרויקט ריאקט: צריך שיהיה לכם פרויקט ריאקט קיים או צרו אחד חדש באמצעות Create React App או כלי דומה.
התקנה
התקינו את החבילות הבאות באמצעות npm או yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
או, באמצעות yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
הסבר על החבילות:
- @testing-library/react: הספרייה המרכזית לבדיקת קומפוננטות ריאקט.
- @testing-library/jest-dom: מספקת matchers מותאמים אישית של Jest לביצוע הצהרות (assertions) על צומתי DOM.
- Jest: פריימוורק בדיקות JavaScript פופולרי.
- babel-jest: טרנספורמר של Jest המשתמש ב-Babel כדי לקמפל את הקוד שלכם.
- @babel/preset-env: פריסט (preset) של Babel הקובע את הפלאגינים והפריסטים של Babel הדרושים לתמיכה בסביבות היעד שלכם.
- @babel/preset-react: פריסט של Babel עבור ריאקט.
תצורה
צרו קובץ `babel.config.js` בשורש הפרויקט שלכם עם התוכן הבא:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
עדכנו את קובץ ה-`package.json` שלכם כדי שיכלול סקריפט בדיקה:
{
"scripts": {
"test": "jest"
}
}
צרו קובץ `jest.config.js` בשורש הפרויקט שלכם כדי להגדיר את Jest. תצורה מינימלית עשויה להיראות כך:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
צרו קובץ `src/setupTests.js` עם התוכן הבא. זה מבטיח שה-matchers של Jest DOM יהיו זמינים בכל הבדיקות שלכם:
import '@testing-library/jest-dom/extend-expect';
כתיבת הבדיקה הראשונה שלכם
בואו נתחיל עם דוגמה פשוטה. נניח שיש לכם קומפוננטת ריאקט המציגה הודעת ברכה:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
כעת, בואו נכתוב בדיקה עבור קומפוננטה זו:
// 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();
});
הסבר:
- `render`: פונקציה זו מרנדרת את הקומפוננטה לתוך ה-DOM.
- `screen`: אובייקט זה מספק מתודות לשאילתות על ה-DOM.
- `getByText`: מתודה זו מוצאת אלמנט לפי תוכן הטקסט שלו. הדגל `/i` הופך את החיפוש ללא תלות באותיות רישיות (case-insensitive).
- `expect`: פונקציה זו משמשת לביצוע הצהרות (assertions) לגבי התנהגות הקומפוננטה.
- `toBeInTheDocument`: matcher זה מצהיר שהאלמנט קיים ב-DOM.
כדי להריץ את הבדיקה, בצעו את הפקודה הבאה בטרמינל שלכם:
npm test
אם הכל מוגדר כראוי, הבדיקה אמורה לעבור.
שאילתות (Queries) נפוצות ב-RTL
RTL מספקת מתודות שאילתה שונות למציאת אלמנטים ב-DOM. שאילתות אלו נועדו לחקות את האופן שבו משתמשים מתקשרים עם האפליקציה שלכם.
`getByRole`
שאילתה זו מוצאת אלמנט לפי תפקיד ה-ARIA שלו. זוהי פרקטיקה טובה להשתמש ב-`getByRole` ככל האפשר, מכיוון שהיא מקדמת נגישות ומבטיחה שהבדיקות שלכם יהיו עמידות לשינויים במבנה ה-DOM הבסיסי.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
שאילתה זו מוצאת אלמנט לפי הטקסט של התווית (label) המשויכת אליו. היא שימושית לבדיקת רכיבי טופס.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
שאילתה זו מוצאת אלמנט לפי טקסט ה-placeholder שלו.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
שאילתה זו מוצאת אלמנט תמונה לפי טקסט ה-alt שלו. חשוב לספק טקסט alt משמעותי לכל התמונות כדי להבטיח נגישות.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
שאילתה זו מוצאת אלמנט לפי מאפיין ה-title שלו.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
שאילתה זו מוצאת אלמנט לפי ערך התצוגה שלו. זה שימושי לבדיקת קלטים בטפסים עם ערכים שמולאו מראש.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
שאילתות `getAllBy*`
בנוסף לשאילתות `getBy*`, RTL מספקת גם שאילתות `getAllBy*`, המחזירות מערך של אלמנטים תואמים. אלה שימושיות כאשר אתם צריכים להצהיר שקיימים מספר אלמנטים עם אותם מאפיינים ב-DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
שאילתות `queryBy*`
שאילתות `queryBy*` דומות לשאילתות `getBy*`, אך הן מחזירות `null` אם לא נמצא אלמנט תואם, במקום לזרוק שגיאה. זה שימושי כאשר אתם רוצים להצהיר שאלמנט *אינו* קיים ב-DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
שאילתות `findBy*`
שאילתות `findBy*` הן גרסאות אסינכרוניות של שאילתות `getBy*`. הן מחזירות Promise שנפתר (resolves) כאשר האלמנט התואם נמצא. אלה שימושיות לבדיקת פעולות אסינכרוניות, כגון שליפת נתונים מ-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();
});
הדמיית אינטראקציות משתמש
RTL מספקת את ממשקי ה-API `fireEvent` ו-`userEvent` להדמיית אינטראקציות משתמש, כגון לחיצה על כפתורים, הקלדה בשדות קלט ושליחת טפסים.
`fireEvent`
`fireEvent` מאפשר לכם להפעיל אירועי DOM באופן פרוגרמטי. זהו API ברמה נמוכה יותר המעניק לכם שליטה מדויקת על האירועים המופעלים.
<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` הוא API ברמה גבוהה יותר המדמה אינטראקציות משתמש בצורה מציאותית יותר. הוא מטפל בפרטים כמו ניהול פוקוס וסדר אירועים, מה שהופך את הבדיקות שלכם לחזקות יותר ופחות שבירות.
<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!');
});
בדיקת קוד אסינכרוני
אפליקציות ריאקט רבות כוללות פעולות אסינכרוניות, כגון שליפת נתונים מ-API. RTL מספקת מספר כלים לבדיקת קוד אסינכרוני.
`waitFor`
`waitFor` מאפשר לכם להמתין עד שתנאי מסוים יתקיים לפני ביצוע הצהרה. זה שימושי לבדיקת פעולות אסינכרוניות שלוקח להן זמן מה להסתיים.
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*`
כפי שצוין קודם לכן, שאילתות `findBy*` הן אסינכרוניות ומחזירות Promise שנפתר כאשר האלמנט התואם נמצא. אלה שימושיות לבדיקת פעולות אסינכרוניות הגורמות לשינויים ב-DOM.
בדיקת Hooks
React Hooks הם פונקציות רב-פעמיות המכמסלות לוגיקה עם מצב (stateful logic). RTL מספקת את כלי העזר `renderHook` מ-`@testing-library/react-hooks` (שהשימוש בו הוצא משימוש לטובת `@testing-library/react` ישירות החל מגרסה 17) לבדיקת Hooks מותאמים אישית בבידוד.
// 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);
});
הסבר:
- `renderHook`: פונקציה זו מרנדרת את ה-Hook ומחזירה אובייקט המכיל את התוצאה של ה-Hook.
- `act`: פונקציה זו משמשת לעטוף כל קוד הגורם לעדכוני מצב. זה מבטיח שריאקט יכולה לאגד ולעבד את העדכונים כראוי.
טכניקות בדיקה מתקדמות
לאחר ששלטתם ביסודות של RTL, תוכלו לחקור טכניקות בדיקה מתקדמות יותר כדי לשפר את האיכות והתחזוקתיות של הבדיקות שלכם.
Mocking של מודולים
לפעמים, ייתכן שתצטרכו לעשות mock (לדמות) מודולים או תלויות חיצוניות כדי לבודד את הקומפוננטות שלכם ולשלוט בהתנהגותן במהלך הבדיקה. Jest מספק API רב עוצמה של mocking למטרה זו.
// 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);
});
הסבר:
- `jest.mock('../api/dataService')`: שורה זו עושה mock למודול `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: שורה זו מגדירה את הפונקציה המדומה `fetchData` להחזיר Promise שנפתר עם הנתונים שצוינו.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: שורה זו מצהירה שהפונקציה המדומה `fetchData` נקראה פעם אחת.
Context Providers
אם הקומפוננטה שלכם מסתמכת על Context Provider, תצטרכו לעטוף את הקומפוננטה שלכם ב-provider במהלך הבדיקה. זה מבטיח שלקומפוננטה תהיה גישה לערכי ה-context.
// 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();
});
הסבר:
- אנו עוטפים את `MyComponent` ב-`ThemeProvider` כדי לספק את ה-context הדרוש במהלך הבדיקה.
בדיקה עם Router
כאשר בודקים קומפוננטות המשתמשות ב-React Router, תצטרכו לספק context מדומה של Router. ניתן להשיג זאת באמצעות הקומפוננטה `MemoryRouter` מ-`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');
});
הסבר:
- אנו עוטפים את `MyComponent` ב-`MemoryRouter` כדי לספק context מדומה של Router.
- אנו מצהירים שלאלמנט הקישור יש את מאפיין ה-`href` הנכון.
שיטות עבודה מומלצות לכתיבת בדיקות יעילות
להלן מספר שיטות עבודה מומלצות שיש לפעול לפיהן בעת כתיבת בדיקות עם RTL:
- התמקדו באינטראקציות משתמש: כתבו בדיקות המדמות כיצד משתמשים מתקשרים עם האפליקציה שלכם.
- הימנעו מבדיקת פרטי יישום: אל תבדקו את המנגנונים הפנימיים של הקומפוננטות שלכם. במקום זאת, התמקדו בהתנהגות הנצפית.
- כתבו בדיקות ברורות ותמציתיות: הפכו את הבדיקות שלכם לקלות להבנה ולתחזוקה.
- השתמשו בשמות בדיקה משמעותיים: בחרו שמות בדיקה המתארים במדויק את ההתנהגות הנבדקת.
- שמרו על בדיקות מבודדות: הימנעו מתלויות בין בדיקות. כל בדיקה צריכה להיות עצמאית ועומדת בפני עצמה.
- בדקו מקרי קצה: אל תבדקו רק את התרחיש האופטימי. הקפידו לבדוק גם מקרי קצה ומצבי שגיאה.
- כתבו בדיקות לפני הקוד: שקלו להשתמש בפיתוח מונחה-בדיקות (TDD) כדי לכתוב בדיקות לפני שאתם כותבים את הקוד.
- פעלו לפי תבנית "AAA": סדר (Arrange), פעל (Act), הצהר (Assert). תבנית זו עוזרת לבנות את הבדיקות שלכם ולהפוך אותן לקריאות יותר.
- שמרו על מהירות הבדיקות שלכם: בדיקות איטיות עלולות להרתיע מפתחים מלהריץ אותן לעיתים קרובות. בצעו אופטימיזציה למהירות הבדיקות על ידי דימוי בקשות רשת ומזעור כמות המניפולציות ב-DOM.
- השתמשו בהודעות שגיאה תיאוריות: כאשר הצהרות נכשלות, הודעות השגיאה צריכות לספק מספיק מידע כדי לזהות במהירות את הגורם לכישלון.
סיכום
React Testing Library הוא כלי רב עוצמה לכתיבת בדיקות יעילות, קלות לתחזוקה וממוקדות-משתמש עבור אפליקציות הריאקט שלכם. על ידי יישום העקרונות והטכניקות המתוארים במדריך זה, תוכלו לבנות אפליקציות חזקות ואמינות העונות על צרכי המשתמשים שלכם. זכרו להתמקד בבדיקה מנקודת המבט של המשתמש, להימנע מבדיקת פרטי יישום, ולכתוב בדיקות ברורות ותמציתיות. על ידי אימוץ RTL ושיטות עבודה מומלצות, תוכלו לשפר משמעותית את האיכות והתחזוקתיות של פרויקטי הריאקט שלכם, ללא קשר למיקומכם או לדרישות הספציפיות של הקהל הגלובלי שלכם.