توسعه فرانت‌اند

Testing Library و تست کامپوننت React

تفاوت runner و DOM، queryهای زمانی و معنایی، user-event و محدودیت jsdom؛ چهار فایل کامل نمونه toggle با وابستگی‌ها و فرمان اجرا.

1404/11/06 2 دقیقه مطالعه 1,346 بازدید

یک دکمه بعد از انتخاب، متن و وضعیتش را تغییر می‌دهد. آزمونی که فقط مقدار یک متغیر داخلی را بررسی می‌کند ممکن است سبز شود، در حالی که کاربر هنوز عنوان قبلی را می‌بیند. React Testing Library امکان کار با DOM رندرشده را فراهم می‌کند تا قرارداد قابل مشاهده مؤلفه بررسی شود. برای نوشتن آزمون مفید، ابتدا باید بگوییم مخاطب چه کاری انجام می‌دهد و چه نتیجه‌ای باید دریافت کند.

این کتابخانه جای تمام انواع ارزیابی را نمی‌گیرد. رفتار یک مؤلفه در DOM، چیدمان واقعی، ارتباط شبکه و جریان کامل محصول دامنه‌های متفاوتی دارند. انتخاب ابزار از سؤال آزمون می‌آید. برای toggle ساده، بررسی نقش دکمه و وضعیت آن می‌تواند کافی باشد؛ برای پرداخت، تنها رندر مؤلفه اطمینان لازم درباره ثبت سفارش نمی‌دهد. ادعای اینکه شبیه‌سازی تعامل همیشه رفتار واقعی را تضمین می‌کند دقیق نیست.

Runner، محیط و کتابخانه وظیفه جدا دارند

مستندات React Testing Library آن را ابزار کار با مؤلفه‌های React معرفی می‌کند، نه test runner. Runner فایل‌ها و نتیجه آزمون را مدیریت می‌کند. محیطی مانند jsdom بخشی از APIهای DOM را فراهم می‌کند و jest-dom matcherهای مرتبط با DOM اضافه می‌کند. شناخت این مرزها کمک می‌کند خطای نصب را با شکست رفتار مؤلفه اشتباه نگیریم.

در نمونه این مقاله از Vitest استفاده می‌شود. راهنمای Vitest پیش‌نیاز محیط و نصب را توضیح می‌دهد؛ نسخه Node.js و بسته‌های قفل‌شده پروژه باید سازگار باشند. برای پوشه آموزشی تازه می‌توانید از Node.js 24 استفاده کنید. این انتخاب نسخه برای نمونه است؛ در پروژه موجود، تنظیمات runner را با قرارداد همان مخزن هماهنگ کنید و فایل‌های پیکربندی را بی‌دلیل جایگزین نکنید.

Query را با انتظار زمانی انتخاب کنید

راهنمای Queryها تفاوت getBy، queryBy و findBy را شرح می‌دهد. اگر عنصر اکنون باید وجود داشته باشد، getBy مناسب است. برای بررسی نبودن، queryBy امکان نتیجه null می‌دهد. برای عنصری که بعداً ظاهر می‌شود، findBy انتظار async را فراهم می‌کند. استفاده از query نامناسب ممکن است آزمون را شکننده کند یا شکست اصلی را پشت خطای زمان پنهان کند.

نقش و نام قابل دسترس، label و متن مسیرهای مفیدی برای یافتن عنصرند. اگر دکمه نام روشنی ندارد، اضافه کردن test id مسئله استفاده را اصلاح نمی‌کند. البته data-testid همیشه ممنوع نیست؛ گاهی عنصر معنی مناسبی برای query معنایی ندارد. تصمیم را با نقش واقعی عنصر بگیرید. نوشتن selector بر اساس زنجیره کلاس‌ها معمولاً آزمون را به تغییر ظاهر و ساختار داخلی حساس می‌کند.

نمونه کامل علاقه‌مندی محلی

در یک پوشه تازه، npm init -y را اجرا و سپس npm pkg set type=module را اعمال کنید. با npm install react react-dom وابستگی‌های React را نصب کنید. بعد با npm install -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom ابزارهای نمونه را اضافه کنید. package-lock.json را نگه دارید. این نمونه JavaScript و JSX است و TypeScript یا API خارجی ندارد.

چهار فایل بعدی را با نام‌های گفته‌شده در ریشه همان پوشه ذخیره کنید. vitest.config.js محیط jsdom و setup.js را معرفی می‌کند. این تنظیم فقط برای پروژه آموزشی مستقل است. اگر پروژه شما تنظیم Vite یا Vitest موجود دارد، همان گزینه‌های لازم را در قرارداد فعلی ادغام کنید. افزودن پیکربندی دوم بدون توجه به اولویت بارگذاری می‌تواند نتیجه‌ای متفاوت از انتظار ایجاد کند.

import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['./setup.js'], globals: false } });

فایل setup.js matcherها را وارد و cleanup را پس از هر آزمون فراخوانی می‌کند. در این مثال globals خاموش است، بنابراین توابع runner از vitest import می‌شوند. پاک شدن DOM میان آزمون‌ها از تداخل وضعیت نمونه‌ها جلوگیری می‌کند. این پاک‌سازی به‌تنهایی همه منابع بیرونی یا timerهای سفارشی را مدیریت نمی‌کند؛ هر منبع اضافه قرارداد عمر جدا می‌خواهد.

import '@testing-library/jest-dom/vitest';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(() => cleanup());

فایل Favorite.jsx یک button واقعی و یک پیام وضعیت دارد. عنوان دکمه ثابت می‌ماند و aria-pressed انتخاب‌شده بودن را بیان می‌کند. عمل فقط state محلی مؤلفه را عوض می‌کند و ذخیره روی سرور ندارد. برای توضیح بازخورد این نوع کنترل، راهنمای Microinteractions مکمل است. مقدار داخلی state در آزمون مستقیماً خوانده نمی‌شود؛ نتیجه قابل مشاهده بررسی می‌شود.

import React, { useState } from 'react';
export function Favorite() {
  const [selected, setSelected] = useState(false);
  return <section aria-label="علاقه‌مندی مقاله">
    <button type="button" aria-pressed={selected} onClick={() => setSelected(value => !value)}>علاقه‌مندی مقاله</button>
    <p role="status">{selected ? 'مقاله انتخاب شد.' : 'مقاله انتخاب نشده است.'}</p>
  </section>;
}

فایل Favorite.test.jsx ابتدا وضعیت غیرفعال را بررسی می‌کند، سپس با userEvent انتخاب و برداشتن انتخاب را انجام می‌دهد. پس از هر عمل، هم وضعیت دکمه و هم متن نتیجه بررسی می‌شوند. این قرارداد دو جهت تعامل را پوشش می‌دهد و به اسم متغیر داخلی وابسته نیست. آزمون درباره ذخیره پایدار یا شبکه ادعایی ندارد، چون مؤلفه چنین رفتاری پیاده نکرده است.

import React from 'react';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Favorite } from './Favorite.jsx';
test('مخاطب می‌تواند علاقه‌مندی را انتخاب و سپس بردارد', async () => {
  const user = userEvent.setup();
  render(<Favorite />);
  const button = screen.getByRole('button', { name: 'علاقه‌مندی مقاله' });
  expect(button).toHaveAttribute('aria-pressed', 'false');
  expect(screen.getByRole('status')).toHaveTextContent('مقاله انتخاب نشده است.');
  await user.click(button);
  expect(button).toHaveAttribute('aria-pressed', 'true');
  expect(screen.getByRole('status')).toHaveTextContent('مقاله انتخاب شد.');
  await user.click(button);
  expect(button).toHaveAttribute('aria-pressed', 'false');
  expect(screen.getByRole('status')).toHaveTextContent('مقاله انتخاب نشده است.');
});

برای اجرای یک‌باره نمونه، فرمان npx vitest run را در همان پوشه استفاده کنید. فایل‌ها باید با نام‌های بالا کنار یکدیگر باشند تا import نسبی پیدا شود. کد config برای JSX import شده از React استفاده می‌کند و افزونه React جدا لازم ندارد. این راهنما روش اجرا را ارائه می‌کند؛ نتیجه آزمون را باید از اجرای پروژه در محیط سازگار خودتان دریافت کنید.

تعامل را به رویداد منفرد تقلیل ندهید

مستندات user-event تفاوت شبیه‌سازی تعامل با dispatch یک رویداد منفرد را توضیح می‌دهد. setup یک نمونه کاربر برای جریان آزمون می‌سازد و عملیات async با await فراخوانی می‌شوند. فراموش کردن انتظار می‌تواند assertion را پیش از پایان تعامل اجرا کند. برای کنترل پیچیده، انتخاب شیوه تعامل باید با رفتار مورد انتظار هماهنگ باشد و وضعیت disabled یا focus هم در نظر گرفته شود.

fireEvent هنوز برای بعضی رخدادهای سطح پایین کاربرد دارد؛ استفاده از آن همیشه نادرست نیست. مهم است آزمون چه چیزی را اثبات می‌کند. فشردن یک دکمه با کلیک، تایپ در کادر و حرکت با صفحه‌کلید جریان‌های متفاوت‌اند. اگر فقط handler را مستقیم صدا می‌زنید، احتمالاً قرارداد اتصال کنترل و تعامل را بررسی نکرده‌اید. نام آزمون باید همین دامنه را روشن بیان کند.

Async و Mock نیاز به قرارداد دارند

برای داده‌ای که پس از درخواست می‌آید، حالت انتظار، نتیجه و خطا را جدا تعریف کنید. تأخیر ثابت در آزمون جای انتظار بر شرط قابل مشاهده را نمی‌گیرد. Mock پاسخ شبکه باید با شکل واقعی داده سازگار باشد، وگرنه آزمون فقط رابطه دو فرض ساختگی را تأیید می‌کند. خطای شبکه و پاسخ نامعتبر نیز دو حالت متفاوت‌اند و ممکن است پیام‌ها و مسیرهای اصلاح جدا بخواهند.

Mock کمتر یک توصیه کلی است، نه هدف عددی. اگر رفتار بیرونی قابل کنترل نیست، جداسازی آن می‌تواند آزمون را قابل اعتماد کند. اما Mock کردن خود مؤلفه‌ای که قرار است رفتار آن بررسی شود سؤال آزمون را خالی می‌کند. برای APIهای مرتبط، راهنمای قرارداد REST API درباره شکل نتیجه و خطا توضیح دارد. مرز مؤلفه و سرویس را آگاهانه انتخاب کنید.

jsdom مرورگر واقعی نیست

محیط DOM برای بسیاری از قراردادهای مؤلفه مناسب است، اما اندازه‌گیری واقعی layout، رفتار بصری و بعضی APIهای مرورگر را کامل فراهم نمی‌کند. سبز شدن آزمون درباره overflow یا دیده شدن دکمه در همه اندازه‌ها نتیجه قطعی نمی‌دهد. برای سؤال‌های بصری و جریان کامل، روش دیگری لازم می‌شود. دامنه آزمون را در توضیح نتیجه نگه دارید تا اطمینان بیشتر از شواهد تولید نشود.

Query بر اساس role می‌تواند نبودن نام مناسب را آشکار کند، ولی ارزیابی کامل دسترس‌پذیری نیست. کنتراست، ترتیب منطقی، خبرهای مزاحم و استفاده با ابزارهای کمکی نیاز به توجه جدا دارند. در Dialog، برای مثال، مدیریت focus و بازگشت قراردادی فراتر از وجود یک پنجره در DOM است. آزمون باید سؤال روشن داشته باشد و محدودیت پاسخ آن معلوم بماند.

آزمون قابل نگهداری

نام آزمون را از رفتار بنویسید، نه از نام تابع داخلی. داده نمونه حداقل چیزی باشد که قرارداد را نشان می‌دهد و failure پیام قابل فهم بدهد. Snapshot بزرگ ممکن است تغییر را ثبت کند ولی دلیل درستی آن را توضیح نمی‌دهد. برای یک تعامل ساده، assertionهای مرتبط با همان عمل اغلب روشن‌ترند. پوشش عددی بالا هم به‌تنهایی کیفیت سؤال‌های آزمون را ثابت نمی‌کند.

در تحویل، پیش‌نیازها، نام فایل‌ها، فرمان اجرا و مرز نمونه را مشخص کنید. نتیجه قابل اتکا زمانی به دست می‌آید که رفتار مورد انتظار، query و تعامل با هم هماهنگ باشند. Testing Library ابزار این هماهنگی را فراهم می‌کند؛ انتخاب سؤال، داده و دامنه همچنان کار توسعه‌دهنده است. آزمون مفید تغییر داخلی بی‌اثر را تحمل می‌کند و شکستن قرارداد واقعی کاربر را واضح نشان می‌دهد.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!