צעדים ראשונים עם react-testing-library
פוסט זה כולל טיפ קצר לעבודה עם React. אם אתם רוצים ללמוד איתי ריאקט מההתחלה ובצורה מקצועית תשמחו לשמוע שבניתי קורס מלא הכולל עשרות שיעורי וידאו והמון תרגול בו לומדים ריאקט מההתחלה ועד לנושאים המתקדמים.
לפרטים נוספים והרשמה בקרו בדף קורס ריאקט כאן באתר.
ספריית react-testing-library היא היום הדרך המקובלת לכתוב בדיקות ליישומי ריאקט. קוד rtl נוצר כל פעם שאנחנו בונים אפליקציה חדשה עם create-react-app וגם הסקריפטים הדרושים להריץ אותו. בואו נראה איך להתחיל להשתמש בספריה כדי לכתוב בדיקות ולמה זה כדאי.
1. שיטת העבודה עם react-testing-library
מנגנון העבודה של react-testing-library הוא לא אינטואיטיבי: היא לא מאפשרת לנו גישה ישירה למאפייני הקומפוננטה, ובמקום זה מעודדת אותנו לכתוב את הבדיקה "מבחוץ", אנחנו מסתכלים על הקומפוננטה מבחוץ, שולחים אליה אירועים ומסתכלים על התוצאה.
הייתרון בגישה הוא שהבדיקות יותר עמידות ל Refactoring בהשוואה לשיטות אחרות. אם שיניתי שמות או סוגים של משתני State והקומפוננטה עדיין עובדת כמו שהיא צריכה לעבוד הבדיקה תמשיך לעבור. החיסרון הוא שלפעמים יותר קשה לכתוב את הבדיקות בהשוואה לספריה כמו Enzyme.
2. דוגמה ראשונה - בדיקת כפתור שמשנה קלאס
נתחיל עם דוגמה ראשונה לקומפוננטה וקוד הבדיקה שלה, ובשביל הפתיחה אקח קומפוננטה שכוללת כפתור שמשנה את צבע הרקע של חלק מסוים בקומפוננטה. הכפתור מוסיף ומוריד קלאס בשם selected, וקוד CSS יהיה אחראי על שינוי הצבעים.
קוד הקומפוננטה יישמר בקובץ App.tsx ונראה כך:
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [toggle, setToggle] = useState(false);
return (
<div className="App">
<button onClick={() => setToggle(t => !t)}>Click me</button>
<header className={`App-header ${toggle ? "selected" : ""}`}>
<img src={logo} className="App-logo" alt="logo" />
</header>
</div>
);
}
export default App;
וקוד הבדיקה יישמר בקובץ App.test.tsx ונראה כך:
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import App from './App';
test('Click me button is there', () => {
const { container, getByText } = render(<App />);
const btn = getByText(/Click me/i);
const header = container.querySelector('header');
expect(header.classList.contains('selected')).toBe(false);
fireEvent.click(btn);
expect(header.classList.contains('selected')).toBe(true);
fireEvent.click(btn);
expect(header.classList.contains('selected')).toBe(false);
});
הפתעה ראשונה בקוד הבדיקות היא הקריאה ל getByText כדי לקבל את הכפתור. זה מפתיע כי בשום מקום אין כאן התיחסות לזה שהכפתור הוא אלמנט מסוג button, ופה אנחנו רואים את תפיסת העולם של react-testing-library: אנחנו בודקים מה שמשתמש רואה.
הפונקציה render מייצרת את הקומפוננטה ומחזירה אוביקט שבעזרתו אנחנו יכולים להגיע לדבר שנבנה. המשתנה container בתוך אותו אוביקט מייצג את ה DOM Element שמתאים לקומפוננטה, והמשתנה getByText הוא פונקציה שבעזרתה אפשר להגיע לאלמנטים בתוך הקומפוננטה (אגב בתיעוד של react-testing-library תוכלו למצוא את כל שאר הפונקציות שיש באוביקט החזרה).
הממשק מאוד דומה ל Jasmine, מוקה ושאר ספריות הבדיקה, ואנחנו יכולים לשלוח אירועים לקומפוננטה עם fireEvent.
מה שהבדיקה עושה זה לשלוח אירוע Click לכפתור ולראות שנוסף הקלאס selected על אלמנט ה header. אחרי זה ממשיכים ללחוץ ורואים שהקלאס יורד ומתווסף מחדש עם כל לחיצה.
3. דוגמה שניה - בדיקת ערכים שעוברים כ Props לקומפוננטה
בשביל לבדוק קומפוננטה שמקבלת ערכים כ props כל מה שצריך לעשות הוא להוסיף ערכים אלה בשורת ה render של הבדיקה. נניח שהקומפוננטה שלי נראית כך:
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App(props: { name: string }) {
const { name } = props;
return (
<div className="App">
<p>Hello {name}</p>
</div>
);
}
export default App;
היא מקבלת פרמטר בשם name ומציגה את הטקסט Hello והשם שהיא קיבלה. בשביל לבדוק את זה נמשוך את הטקסט מהקומפוננטה ונבדוק שהוא שווה למה שאנחנו מצפים:
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import App from './App';
test('Click me button is there', () => {
const { container, getByText } = render(<App name="ynon"/>);
const p = getByText(/Hello /);
expect(p.textContent).toEqual("Hello ynon");
});
4. דוגמה שלישית - בדיקת קומפוננטה שטוענת מידע מרחוק
קומפוננטה שלישית ואחרונה בסידרה היא כזו שטוענת מידע מהרשת באמצעות swr. הקומפוננטה שלנו תקבל כ prop שם משתמש גיטהאב ותציג על המסך את מספר הריפוזיטוריז הציבוריים של אותו משתמש. הנה הקוד:
import useSWR from 'swr'
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css';
const fetcher = (url: string) => fetch(url).then(data => data.json());
function App(props: { username: string }) {
const { username } = props;
const { data, error } = useSWR('https://api.github.com/users/ynonp', fetcher)
if (!data) { return <p>Loading...</p> }
const numRepos = data.public_repos;
return (
<div className="App">
<p>User has {numRepos} repositories</p>
</div>
);
}
export default App;
בדיקה ראשונה שאפשר לחשוב עליה היא לוודא שהטקסט Loading מוצג כשהקומפוננטה עולה. אנחנו כבר יודעים איך לכתוב בדיקה כזו והקוד נראה כך:
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import App from './App';
test('Shows loading until ready', () => {
const { container, getByText } = render(<App username="ynonp"/>);
const p = getByText(/Loading/);
expect(p).toBeInTheDocument();
});
הבדיקה השניה קצת יותר מעניינת: נרצה לבדוק שהמידע נטען ומוצג כמו שצריך. בשביל זה נכתוב בדיקה אסינכרונית ונשתמש בפקודה waitForElement של react-testing-library שתחכה שאלמנט מסוים יופיע על המסך. הנה הקוד:
import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import App from './App';
test('Shows repo count', async () => {
const { container, getByText } = render(<App username="ynonp"/>);
const countEl = await waitForElement(() => getByText(/User has/));
expect(countEl).toBeInTheDocument();
});
העבודה עם react-testing-library יכולה להיראות מעייפת בהתחלה כי ה API של הספריה יחסית ייחודי ולוקח זמן להתרגל אליו. אחרי שמתרגלים מגלים ספריה מאוד ממוקדת בבניית קוד בדיקות שכוללת את כל הפונקציות שצריך ועוזרת לנו להתמקד בכתיבת הבדיקות הנכונות. כמובן שמאוד נוח להשאיר טאב של npm test רץ כל הזמן במהלך הפיתוח וכך אנחנו רואים את הקוד ששברנו בשניה ששברנו אותו.