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

تست کامپوننت مکان خوبی برای شروع نمایش کدهای آزمایشی عملی است. تست‌های کامپوننت از تست‌های واحد ساده‌تر، پیچیده‌تر از تست‌های انتها به انتها هستند و تعامل با 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 مسخره شده در مثال قبلی تعامل دارد، و آزمایش می‌تواند تأیید کند که شناسه ارسال شده "سام" بوده است:

  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
کله زدن جهانیان
بررسی اینکه یک عدد افزایش یافته است