בדיקות עם Jest ו-React Testing Library
מבוא
בדיקות אוטומטיות מעניקות ביטחון שהקוד ממשיך לעבוד גם אחרי שינויים. שני כלים משלימים מובילים בעולם ה-React: Jest - סביבת הרצת הבדיקות (test runner) שמריצה את הבדיקות ומספקת פונקציות השוואה (assertions), ו-React Testing Library (RTL) - ספרייה שבודקת קומפוננטות מנקודת מבטו של המשתמש, כלומר לפי מה שמוצג על המסך ולא לפי פרטי המימוש הפנימיים.
התקנה
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest
מבנה בסיסי של בדיקה
// Component.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
test('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('שלום')).toBeInTheDocument();
});
});
render ו-screen
import { render, screen } from '@testing-library/react';
test('renders button with text', () => {
// render - מרנדר את הקומפוננטה
render(<Button>לחץ כאן</Button>);
// screen - גישה לאלמנטים שרונדרו
const button = screen.getByRole('button', { name: /לחץ כאן/i });
expect(button).toBeInTheDocument();
});
Queries - חיפוש אלמנטים
סוגי Queries
| Query | מתי להשתמש |
|---|---|
getBy... |
האלמנט חייב להיות קיים |
queryBy... |
האלמנט עלול לא להיות קיים |
findBy... |
אלמנט אסינכרוני (Promise) |
שיטות חיפוש
// לפי Role (מומלץ!)
screen.getByRole('button');
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /אימייל/i });
// לפי Label
screen.getByLabelText('סיסמה');
// לפי Placeholder
screen.getByPlaceholderText('הקלד כאן...');
// לפי טקסט
screen.getByText('שלום עולם');
screen.getByText(/שלום/i); // case-insensitive
// לפי Test ID
screen.getByTestId('submit-button');
// לפי Alt
screen.getByAltText('לוגו החברה');
דוגמאות
test('form elements', () => {
render(<LoginForm />);
// getBy - זורק שגיאה אם לא נמצא
const emailInput = screen.getByLabelText(/אימייל/i);
const passwordInput = screen.getByLabelText(/סיסמה/i);
const submitButton = screen.getByRole('button', { name: /התחבר/i });
// queryBy - מחזיר null אם לא נמצא
const error = screen.queryByText(/שגיאה/i);
expect(error).not.toBeInTheDocument();
});
User Events - אינטראקציות
הספרייה user-event מדמה אינטראקציות אמיתיות של משתמש - הקלדה, לחיצה, בחירה מרשימה ועוד - בצורה קרובה יותר להתנהגות בדפדפן. מכיוון שהפעולות אסינכרוניות, מקדימים להן await:
import userEvent from '@testing-library/user-event';
test('user interactions', async () => {
const user = userEvent.setup();
render(<MyForm onSubmit={handleSubmit} />);
// הקלדה
await user.type(screen.getByLabelText(/שם/i), 'דני');
// לחיצה
await user.click(screen.getByRole('button'));
// Clear + type
await user.clear(screen.getByLabelText(/שם/i));
await user.type(screen.getByLabelText(/שם/i), 'רונית');
// Hover
await user.hover(screen.getByText('עזרה'));
// Select
await user.selectOptions(screen.getByRole('combobox'), 'option1');
// Checkbox
await user.click(screen.getByRole('checkbox'));
});
Assertions - בדיקות
import '@testing-library/jest-dom';
test('assertions', () => {
render(<MyComponent />);
const element = screen.getByText('שלום');
// נוכחות
expect(element).toBeInTheDocument();
expect(screen.queryByText('לא קיים')).not.toBeInTheDocument();
// תוכן
expect(element).toHaveTextContent('שלום');
expect(screen.getByLabelText('שם')).toHaveValue('דני');
// מצב
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByRole('button')).toBeDisabled();
// Style
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
// Attributes
expect(element).toHaveAttribute('href', '/about');
});
דוגמאות מלאות
בדיקת קומפוננטה פשוטה
// Greeting.jsx
function Greeting({ name }) {
return <h1>שלום, {name}!</h1>;
}
// Greeting.test.jsx
test('renders greeting with name', () => {
render(<Greeting name="דני" />);
expect(screen.getByRole('heading')).toHaveTextContent('שלום, דני!');
});
test('renders greeting with different name', () => {
render(<Greeting name="רונית" />);
expect(screen.getByText(/שלום, רונית/i)).toBeInTheDocument();
});
בדיקת Counter
// Counter.jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">{count}</p>
<button onClick={() => setCount(c => c + 1)}>הוסף</button>
<button onClick={() => setCount(c => c - 1)}>הפחת</button>
</div>
);
}
// Counter.test.jsx
describe('Counter', () => {
test('starts at 0', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('0');
});
test('increments on button click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /הוסף/i }));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
test('decrements on button click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /הפחת/i }));
expect(screen.getByTestId('count')).toHaveTextContent('-1');
});
});
בדיקת טופס
// LoginForm.test.jsx
describe('LoginForm', () => {
const mockSubmit = jest.fn();
beforeEach(() => {
mockSubmit.mockClear();
});
test('submits form with valid data', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/אימייל/i), '[email protected]');
await user.type(screen.getByLabelText(/סיסמה/i), '123456');
await user.click(screen.getByRole('button', { name: /התחבר/i }));
expect(mockSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: '123456'
});
});
test('shows error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/אימייל/i), 'invalid');
await user.click(screen.getByRole('button', { name: /התחבר/i }));
expect(screen.getByText(/אימייל לא תקין/i)).toBeInTheDocument();
expect(mockSubmit).not.toHaveBeenCalled();
});
});
בדיקות אסינכרוניות
// async component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>טוען...</p>;
return <h1>{user.name}</h1>;
}
// test
test('loads and displays user', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'דני' })
})
);
render(<UserProfile userId="1" />);
// בהתחלה מציג loading
expect(screen.getByText(/טוען/i)).toBeInTheDocument();
// מחכה לתוכן האסינכרוני
const heading = await screen.findByRole('heading');
expect(heading).toHaveTextContent('דני');
});
Mocking
לעיתים קרובות נרצה לבודד את הקומפוננטה הנבדקת מהתלויות החיצוניות שלה - למשל קריאות לשרת או פונקציות שמועברות כ-props. לשם כך משתמשים ב-Mocks: תחליפים מזויפים שמאפשרים לשלוט בהתנהגות ולוודא שהקומפוננטה קראה להם כצפוי:
Mock Functions
test('calls onClick when clicked', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>לחץ</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
Mock Modules
// __mocks__/api.js
export const fetchUsers = jest.fn();
// Component.test.jsx
jest.mock('./api');
import { fetchUsers } from './api';
beforeEach(() => {
fetchUsers.mockResolvedValue([
{ id: 1, name: 'דני' }
]);
});
test('displays fetched users', async () => {
render(<UserList />);
expect(await screen.findByText('דני')).toBeInTheDocument();
});
Best Practices
- בדקו התנהגות, לא מימוש - התמקדו במה שהמשתמש רואה ועושה, ולא בפרטים הפנימיים של הקוד
- העדיפו חיפוש לפי Role - הוא עמיד יותר ומקדם נגישות, בניגוד ל-
test-id - כתבו בדיקות שמדמות שימוש אמיתי - בדיקה טובה משקפת את דרך העבודה של משתמש חי
- השתמשו ב-
userEventולא ב-fireEvent- הוא מדמה אינטראקציות נאמנה יותר למציאות
סיכום
| API | שימוש |
|---|---|
render() |
רינדור קומפוננטה |
screen |
גישה לאלמנטים |
getBy/queryBy/findBy |
חיפוש אלמנטים |
userEvent |
סימולציית אינטראקציות |
expect() |
assertions |
jest.fn() |
mock function |
הקודם: ← RTK Query
סיום! 🎉
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה הגישה של React Testing Library?
הצגת התשובה
תשובה ד. בדיקות שלא תלויות במימוש נשארות תקינות גם כשמשכתבים קוד.
-
איזו שאילתה עדיפה למציאת כפתור?
הצגת התשובה
תשובה ב. שאילתה לפי תפקיד ושם נגיש בודקת גם שהממשק נגיש.