تست کامپوننت در عمل

تست کامپوننت مکان خوبی برای شروع نمایش کدهای آزمایشی عملی است. تست‌های کامپوننت از تست‌های واحد ساده‌تر، پیچیده‌تر از تست‌های انتها به انتها هستند و تعامل با 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 inside useEffect استفاده می‌کند)، اما پایگاه کد شما احتمالاً شامل موارد زیادی مانند آن است. نکته مهمتر این است که این موارد در نگاه اول ممکن است سخت به نظر برسند. بخش بعدی این دوره به طور مفصل درباره نوشتن کدهای قابل آزمایش بحث خواهد کرد.

مواردی که در این مثال آزمایش می کنیم در اینجا آمده است:

  • بررسی کنید که مقداری DOM صحیح در پاسخ به داده های شبکه ایجاد شود.
  • تأیید کنید که کلیک کردن روی یک کاربر باعث ایجاد یک تماس برگشتی می شود.

هر جزء متفاوت است. چه چیزی آزمایش این یکی را جالب می کند؟

  • از fetch جهانی برای درخواست داده‌های واقعی از شبکه استفاده می‌کند، که ممکن است تحت آزمایش ضعیف یا کند باشند.
  • کلاس دیگری UserRow را وارد می کند که ممکن است نخواهیم به طور ضمنی آن را آزمایش کنیم.
  • از یک Context استفاده می کند که به طور خاص بخشی از کد مورد آزمایش نیست و معمولاً توسط یک مؤلفه والد ارائه می شود.

برای شروع یک تست سریع بنویسید

ما می توانیم به سرعت چیزی بسیار اساسی را در مورد این مؤلفه آزمایش کنیم. برای روشن شدن، این مثال خیلی مفید نیست. اما راه‌اندازی boilerplate در یک فایل همتا به نام 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()

تمسخر عمل جایگزین کردن یک تابع یا کلاس واقعی با چیزی تحت کنترل شما برای آزمایش است. این روش تقریباً در همه انواع آزمون‌ها، به جز ساده‌ترین آزمون‌های واحد، رایج است. این بیشتر در Assertions و دیگر اصول اولیه پوشش داده شده است.

شما می توانید fetch() برای آزمایش خود مسخره کنید تا به سرعت کامل شود و داده های مورد انتظار شما را برگرداند، نه داده های "دنیای واقعی" یا ناشناخته. fetch یک جهانی است، به این معنی که ما مجبور نیستیم آن را به کد خود import یا به آن require .

در vitest، می‌توانید با فراخوانی vi.stubGlobal با یک شی خاص که توسط vi.fn() بازگردانده شده است، یک global را مسخره کنید - این یک ماک می‌سازد که بعداً می‌توانیم آن را تغییر دهیم. این روش‌ها در بخش بعدی این دوره با جزئیات بیشتر مورد بررسی قرار می‌گیرند، اما می‌توانید آنها را به صورت عملی در کد زیر مشاهده کنید:

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 است که در صورت انتخاب کاربر، متد را فراخوانی می کند. این اغلب به‌عنوان جایگزینی برای «حفاری پایه» در نظر گرفته می‌شود، جایی که پاسخ تماس مستقیماً به 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
کله زدن جهانیان