تست کامپوننت مکان خوبی برای شروع نمایش کدهای آزمایشی عملی است. تستهای کامپوننت از تستهای واحد سادهتر، پیچیدهتر از تستهای انتها به انتها هستند و تعامل با DOM را نشان میدهند. از نظر فلسفی تر، استفاده از React باعث شده است که توسعه دهندگان وب راحت تر فکر کنند که وب سایت ها یا برنامه های وب از اجزا تشکیل شده اند.
بنابراین آزمایش اجزای جداگانه، صرف نظر از پیچیدگی آنها، راه خوبی برای شروع به فکر آزمایش یک برنامه جدید یا موجود است.
این صفحه آزمایش یک مؤلفه کوچک با وابستگی های خارجی پیچیده را طی می کند. آزمایش مؤلفهای که با هیچ کد دیگری تعامل ندارد، مانند کلیک کردن روی یک دکمه و تأیید افزایش تعداد، آسان است. در واقع، کد بسیار کمی شبیه آن است و تست کدی که تعاملی ندارد، می تواند ارزش محدودی داشته باشد.
جزء تحت آزمایش
ما از Vitest و محیط JSDOM آن برای آزمایش کامپوننت React استفاده می کنیم. این به ما امکان میدهد در حین شبیهسازی مرورگر، آزمایشها را با استفاده از Node در خط فرمان به سرعت اجرا کنیم.
این کامپوننت 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
آمده باشد.
با این حال، 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 استفاده شد؟