בדיקה בפועל של רכיבים

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

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

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

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

הרכיב בבדיקה

נשתמש ב-Vitest ובסביבת ה-JSDOM שלו כדי לבדוק רכיב של React. כך אנחנו יכולים להריץ בדיקות במהירות באמצעות Node בשורת הפקודה וגם לבצע אמולציה של דפדפן.

רשימת שמות עם לחצן 'בחירה' ליד כל שם.
רכיב תגובה קטן שמציג רשימה של משתמשים מהרשת.

רכיב ה-React שנקרא UserList מאחזר רשימה של משתמשים מהרשת ומאפשר לבחור אחד מהם. רשימת המשתמשים מתקבלת באמצעות fetch בתוך useEffect, וה-handler של הבחירה מועבר על ידי Context. זה הקוד שלו:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

הדוגמה הזו לא מדגימה את השיטות המומלצות של React (למשל, נעשה שימוש ב-fetch בתוך useEffect), אבל סביר להניח שה-codebase מכיל הרבה מקרים כאלה. מעבר לכך, המקרים האלה עלולים להיראות עקשניים לבדיקה במבט ראשון. חלק עתידי בקורס הזה יעסוק בכתיבת קוד הניתן לבדיקה בפירוט.

בדוגמה זו אנו בודקים את הדברים הבאים:

  • יש לוודא שנוצר DOM תקין כלשהו בתגובה לנתונים מהרשת.
  • מוודאים שלחיצה על משתמש גורמת להתקשרות חזרה.

כל רכיב הוא שונה. מה הופך את הבדיקה הזו למעניינת?

  • היא משתמשת בfetch הגלובלי כדי לבקש נתונים אמיתיים מהרשת. נתונים כאלה עשויים להיות לא יציבים או איטיים בבדיקה.
  • המערכת מייבאת כיתה אחרת, UserRow, ויכול להיות שלא נרצה לבדוק אותה באופן לא מפורש.
  • היא משתמשת ב-Context, שהוא לא חלק ספציפי מהקוד שנבדק, ובדרך כלל מסופק על ידי רכיב הורה.

צריך לכתוב בדיקה מהירה כדי להתחיל

נוכל לבדוק במהירות משהו בסיסי מאוד לגבי הרכיב הזה. לצורך הבהרה, דוגמה זו לא מועילה במיוחד! אבל כדאי להגדיר את התבנית סטנדרטית בקובץ עמית בשם UserList.test.tsx (חשוב לזכור שרצי בדיקות כמו Vitest מריצים כברירת מחדל קבצים שמסתיימים ב-.test.js או בסגנון דומה, כולל .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

הבדיקה הזו טוענת שכאשר הרכיב עובר עיבוד, הוא מכיל את הטקסט 'משתמשים'. זה עובד למרות שלרכיב יש תופעת לוואי של שליחת fetch לרשת. השדה fetch עדיין מתבצע בסוף הבדיקה, ללא נקודת קצה (endpoint) מוגדרת. אנחנו לא יכולים לוודא שפרטי המשתמש מוצגים כשהבדיקה מסתיימת, לפחות לא בלי להמתין לזמן קצוב לתפוגה.

הדמיה fetch()

הדמיה היא הפעולה של החלפת פונקציה או כיתה אמיתיות במשהו שנמצא בשליטתכם בבדיקה. זו שיטה מקובלת כמעט בכל סוגי הבדיקות, למעט בדיקות היחידה הפשוטות ביותר. בנושא הזה נרחיב יותר על טענות נכונות (assertions) ופרימיטיבים אחרים.

אפשר לדמות את הבדיקה של fetch() כדי שהיא תושלם במהירות ותציג את הנתונים שציפיתם לראות, ולא נתונים "מהעולם האמיתי" או נתונים לא ידועים. fetch הוא גלובלי, כלומר אנחנו לא צריכים import או require להוסיף אותו לקוד שלנו.

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

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

הקוד הזה מוסיף דוגמה, מתאר גרסה 'מזופת' של אחזור הרשת Response ואז ממתין להופעתה. אם הטקסט לא מופיע, כדי לבדוק את זה אפשר לשנות את השם של השאילתה ב-queryByText לשם חדש. הבדיקה תיכשל.

בדוגמה הזו השתמשנו בכלים המובְנים של Vitest ללעג, אבל ב-frameworks אחרות של בדיקה יש גישות דומות לגבי לעג. בדיקת Vitest היא ייחודית בכך שצריך לקרוא ל-vi.unstubAllGlobals() אחרי כל הבדיקות, או להגדיר אפשרות גלובלית מקבילה. בלי "ביטול" העבודה שלנו, ההדמיה של fetch יכולה להשפיע על בדיקות אחרות, וכל בקשה תקבל מענה באמצעות ערימת ה-JSON המוזרה שלנו!

ייבוא מדומה

ייתכן שהבחנתם שהרכיב UserList שלנו בעצמו מייבא רכיב בשם UserRow. למרות שלא כללנו את הקוד שלו, אפשר לראות שהוא מעבד את שם המשתמש: הבדיקה הקודמת של "Sam" לא מעובדת ישירות בתוך UserList, לכן הוא חייב להגיע מ-UserRow.

תרשים זרימה של האופן שבו שמות המשתמשים עוברים דרך הרכיב שלנו.
ל-UserListTest אין הרשאת גישה של UserRow.

עם זאת, ייתכן ש-UserRow הוא רכיב מורכב – הוא עלול לאחזר נתוני משתמשים נוספים או שיש לו תופעות לוואי שלא רלוונטיות לבדיקה שלנו. אם מסירים את השונות הזו, הבדיקות יהיו מועילות יותר, במיוחד מכיוון שהרכיבים שאתם רוצים לבדוק הופכים למורכבים יותר ומשתלבים יותר עם יחסי התלות שלהם.

למרבה המזל, תוכלו להשתמש ב-Vitest כדי לדמות פריטים ייבוא מסוימים, גם אם בבדיקה לא נעשה בהם שימוש ישיר, כך שכל קוד שמשתמש בהם יסופק עם גרסה פשוטה או ידועה:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

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

צריך ללחוץ ולספק הקשר

React וספריות אחרות כמו Lit כוללות קונספט שנקרא Context. הקוד לדוגמה כולל UserContext שמפעיל שיטה אם בוחרים משתמש. לרוב, אפשר לעשות זאת כחלופה ל"קידוח Pro", שבו הקריאה החוזרת מועברת ישירות אל UserList.

רשת הבדיקה שכתבנו לא סיפקה UserContext. אם מוסיפים פעולת קליק לבדיקת React בלעדיה, זה עלול לגרום לקריסת הבדיקה, או במקרה הטוב ביותר, אם מופע ברירת מחדל סופק במקום אחר, ולא תהיה בשליטתנו (כמו UserRow לא ידוע כפי שמופיע למעלה):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

במקום זאת, בעת עיבוד הרכיב, יש לך אפשרות לספק Context משלך. בדוגמה הזו נשתמש במופע של vi.fn(), פונקציית Vitest Mock, שאפשר להשתמש בה לאחר מעשה כדי לבדוק שבוצעה קריאה, ואילו טיעונים היא נטענה. במקרה שלנו, יש אינטראקציה עם ה-fetch המדומה בדוגמה הקודמת, והבדיקה יכולה לאשר שהמזהה שהועבר היה 'sam':

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

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

בסיכום

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

בחינת ההבנה

באילו גישות השתמשת כדי לבדוק את רכיב ה-React?

הדמיה של יחסי תלות מורכבים עם תנאים פשוטים לבדיקה
הזרקת תלות באמצעות הקשר
לשונית globals
בדיקה שמספר העלייה