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

يعد اختبار المكونات مكانًا جيدًا للبدء في إظهار كود الاختبار العملي. تُعد اختبارات المكونات أهميتها أكثر من اختبارات الوحدات البسيطة وأقل تعقيدًا من الاختبار الشامل وتُظهر التفاعل مع نموذج العناصر في المستند (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>
  );
}

لا يوضّح هذا المثال أفضل الممارسات المتعلقة بالتفاعل (على سبيل المثال، يستخدم السمة 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);
});

يؤكد هذا الاختبار أنه عند عرض المكوِّن، سيحتوي على النص "Users". يعمل مع أنّ المكوِّن له تأثير سلبي يتمثّل في إرسال 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!

عمليات استيراد وهمية

ربما لاحظت أن مكوّن UserList نفسه يستورد مكونًا يسمى UserRow. على الرغم من أنّنا لم نُدرِج رمزه، إلا أنّه يعرض اسم المستخدم: يتحقّق الاختبار السابق من اسم المستخدم "Sam" وأنّه لا يتم عرضه داخل UserList مباشرةً، لذا يجب أن يكون مصدره UserRow.

مخطط انسيابي لطريقة انتقال أسماء المستخدمين في المكوّن الخاص بنا
لا يتوفّر إذن الوصول إلى UserRow للسمة UserListTest.

مع ذلك، قد يكون 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 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).

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

ما هي الأساليب المستخدمة لاختبار مكوِّن التفاعل؟

محاكاة التبعيات المعقدة باستخدام تبعيات بسيطة للاختبار
إضافة التبعية باستخدام السياق
الدبلجة العامة
التحقق من زيادة الرقم