עברית

שלטו ב-React Testing Library (RTL) עם מדריך מקיף זה. למדו כיצד לכתוב בדיקות יעילות, קלות לתחזוקה וממוקדות-משתמש עבור אפליקציות הריאקט שלכם, תוך התמקדות בשיטות עבודה מומלצות ודוגמאות מהעולם האמיתי.

React Testing Library: מדריך מקיף

בנוף פיתוח הרשת המהיר של ימינו, הבטחת האיכות והאמינות של אפליקציות הריאקט שלכם היא בעלת חשיבות עליונה. React Testing Library (RTL) הפכה לפתרון פופולרי ויעיל לכתיבת בדיקות המתמקדות בפרספקטיבה של המשתמש. מדריך זה מספק סקירה מלאה של RTL, המכסה הכל החל מהמושגים הבסיסיים ועד לטכניקות מתקדמות, ומעצים אתכם לבנות אפליקציות ריאקט חזקות וקלות לתחזוקה.

למה לבחור ב-React Testing Library?

גישות בדיקה מסורתיות מסתמכות לעיתים קרובות על פרטי יישום, מה שהופך את הבדיקות לשבירות ונוטות להישבר עם שינויי קוד קלים. RTL, לעומת זאת, מעודדת אתכם לבדוק את הקומפוננטות שלכם כפי שמשתמש היה מתקשר איתן, תוך התמקדות במה שהמשתמש רואה וחווה. גישה זו מציעה מספר יתרונות מרכזיים:

הגדרת סביבת הבדיקות שלכם

לפני שתוכלו להתחיל להשתמש ב-RTL, עליכם להגדיר את סביבת הבדיקות שלכם. זה בדרך כלל כולל התקנת התלויות הנדרשות והגדרת פריימוורק הבדיקות שלכם.

דרישות קדם

התקנה

התקינו את החבילות הבאות באמצעות 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

הסבר על החבילות:

תצורה

צרו קובץ `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();
});

הסבר:

כדי להריץ את הבדיקה, בצעו את הפקודה הבאה בטרמינל שלכם:

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);
});

הסבר:

טכניקות בדיקה מתקדמות

לאחר ששלטתם ביסודות של 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);
});

הסבר:

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();
});

הסבר:

בדיקה עם 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');
});

הסבר:

שיטות עבודה מומלצות לכתיבת בדיקות יעילות

להלן מספר שיטות עבודה מומלצות שיש לפעול לפיהן בעת כתיבת בדיקות עם RTL:

סיכום

React Testing Library הוא כלי רב עוצמה לכתיבת בדיקות יעילות, קלות לתחזוקה וממוקדות-משתמש עבור אפליקציות הריאקט שלכם. על ידי יישום העקרונות והטכניקות המתוארים במדריך זה, תוכלו לבנות אפליקציות חזקות ואמינות העונות על צרכי המשתמשים שלכם. זכרו להתמקד בבדיקה מנקודת המבט של המשתמש, להימנע מבדיקת פרטי יישום, ולכתוב בדיקות ברורות ותמציתיות. על ידי אימוץ RTL ושיטות עבודה מומלצות, תוכלו לשפר משמעותית את האיכות והתחזוקתיות של פרויקטי הריאקט שלכם, ללא קשר למיקומכם או לדרישות הספציפיות של הקהל הגלובלי שלכם.