اختبار المكونات عمليًا

يُعدّ اختبار المكوّنات مكانًا جيدًا للبدء في عرض رمز الاختبار العملي. تُعدّ اختبارات المكونات أكثر أهمية من اختبارات الوحدة البسيطة، وأقل تعقيدًا من الاختبار الشامل، وتُظهر التفاعل مع نموذج العناصر في المستند (DOM). من الناحية المبدئية، سهّل استخدام React على مطوّري الويب التفكير في أنّ المواقع الإلكترونية أو تطبيقات الويب تتألف من مكوّنات.

لذا فإن اختبار المكونات الفردية، بغض النظر عن مدى تعقيدها، يعد طريقة جيدة لبدء التفكير في اختبار تطبيق جديد أو موجود.

تشرح هذه الصفحة كيفية اختبار مكوّن صغير يحتوي على تبعيات خارجية معقدة. من السهل اختبار مكوِّن لا يتفاعل مع أي رمز آخر، مثل النقر على زر وتأكيد زيادة العدد. في الواقع، عدد قليل جدًا من التعليمات البرمجية يشبه ذلك، واختبار التعليمة البرمجية التي لا تحتوي على تفاعلات يمكن أن يكون ذا قيمة محدودة.

المكوّن الذي يتم اختباره

نستخدم Vitest وبيئة JSDOM لاختبار مكوّن React. ويتيح لنا ذلك إجراء اختبارات بسرعة باستخدام عقدة على سطر الأوامر مع محاكاة متصفح.

قائمة بأسماء تتضمّن زر 
    اختيار بجانب كل اسم
مكوّن 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)، ولكن من المرجّح أن تحتوي قاعدة بياناتك على العديد من الحالات مثلها. والأهم من ذلك، قد تبدو هذه الحالات مستعصية على الاختبار عند النظر إليها للمرة الأولى. سيناقش قسم مستقبلي من هذه الدورة التدريبية كتابة رمز قابل للاختبار بالتفصيل.

في ما يلي العناصر التي نختبر أدائها في هذا المثال:

  • تأكَّد من إنشاء بعض عناصر 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 قيد التقدم في نهاية الاختبار، بدون نقطة نهاية محددة. لا يمكننا التأكيد على عرض أي معلومات للمستخدم عند انتهاء الاختبار، على الأقل ليس بدون انتظار انتهاء المهلة.

محاكاة fetch()

التزوير هو عملية استبدال دالة أو فئة حقيقية بشيء تحت إدارتك لإجراء اختبار. وهذه ممارسة شائعة في جميع أنواع الاختبارات تقريبًا، باستثناء أبسط اختبارات الوحدة. يمكنك الاطّلاع على مزيد من المعلومات حول هذا الموضوع في مقالة الافتراضات وغيرها من العناصر الأساسية.

يمكنك محاكاة 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 الفردية من JSON.

استيراد وهمي

ربما لاحظت أنّ المكوّن UserList يستورد مكوّنًا يُسمى UserRow. على الرغم من أنّنا لم نُدرِج رمزه، يمكنك ملاحظة أنّه يعرض اسم المستخدم: يبحث الاختبار السابق عن "سام"، ولا يتم عرضه داخل 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 الذي يُستخدَم لتشغيل الإجراء في حال اختيار مستخدم. غالبًا ما يُنظر إلى ذلك كبديل لـ "التنقيب"، حيث يتم تمرير طلب إعادة الاتصال إلى "UserList" مباشرةً.

لم يوفر متتبع الاختبار UserContext. قد تؤدي إضافة إجراء النقر إلى اختبار React بدونه إلى تعطيل الاختبار في أسوأ الأحوال. في أفضل الأحوال، إذا تم توفير مثيل تلقائي في مكان آخر، قد يؤدي ذلك إلى بعض السلوك العميق الذي لا يمكننا التحكّم فيه (على غرار UserRow غير المعروف أعلاه).

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

بدلاً من ذلك، يمكنك تقديم Context خاص بك عند عرض المكوّن. يستخدم هذا المثال مثيلًا من vi.fn()، وهو دالة محاكاة 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']]);

هذا نمط بسيط ولكنه قوي يمكن أن يتيح لك إزالة التبعيات غير ذات الصلة من المكون الأساسي الذي تحاول اختباره.

الملخّص

توضّح هذا المثال كيفية إنشاء اختبار مكوّن لاختبار وحماية مكوّن React الذي يصعب اختباره. ركّز هذا الاختبار على التأكّد من أنّ ال المكوّن يتفاعل بشكل صحيح مع المكوّنات التابعة له: العنصر الشامل fetch وأحد العناصر الفرعية المستورَدة وContext.

التحقّق من فهمك

ما هي الأساليب التي تم استخدامها لاختبار مكوّن React؟

التحقّق من زيادة رقم
دبلجة عام
محاكاة التبعيات المعقّدة باستخدام تبعيات بسيطة للاختبار
حقن التبعيات باستخدام Context