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

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

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

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

הרכיב שנבדק

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

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

רכיב React בשם UserList מאחזר רשימת משתמשים מהרשת ומאפשר לכם לבחור אחד מהם. רשימת המשתמשים מתקבלת באמצעות fetch בתוך useEffect, ומטפל הבחירה מועבר על ידי 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). אנחנו לא יכולים לאשר אם מידע על משתמשים מוצג בסיום הבדיקה, לפחות לא בלי להמתין לזמן קצוב.

Mock fetch()

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

אפשר ליצור גרסת מודל (mock) של 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 ללעג, אבל במסגרות בדיקה אחרות יש גישות דומות לגבי לעג. 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, שמפעיל את השיטה אם נבחר משתמש. השיטה הזו נחשבת לרוב כחלופה ל'העמקה בנכס', שבה פונקציית ה-callback מועברת ישירות אל UserList.

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

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

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

במקרה שלנו, הוא יוצר אינטראקציה עם 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?

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