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

Vitest: تست سریع برای پروژه‌های Vite

انتخاب environment، تست قرارداد عددی و خطا، watch و run، تفاوت mock و آزمون واقعی و نمونه کامل Vitest برای محاسبه تخفیف.

1404/12/05 3 دقیقه مطالعه 1,532 بازدید

تست باید یک تصمیم برنامه را روشن کند

تابع محاسبهٔ تخفیف ممکن است چند خط باشد، اما تصمیم‌های مهمی دارد: تعداد صفر پذیرفته می‌شود؟ تخفیف صددرصد چه نتیجه‌ای می‌دهد؟ گرد کردن روی هر کالا انجام می‌شود یا روی جمع نهایی؟ اگر این تصمیم‌ها فقط در ذهن نویسنده باشند، تغییر بعدی رفتار را بی‌صدا عوض می‌کند. تست خوب برای همین مرزها مثال مشخص ثبت می‌کند. Vitest ابزاری برای اجرای این مثال‌ها و assertionهاست؛ خودش نمی‌داند قرارداد درست کسب‌وکار چیست.

Vitest از زیرساخت Vite برای تبدیل و بارگذاری کد استفاده می‌کند و می‌تواند در پروژه‌های مختلف به کار رود. نصب Vite در برنامه به معنی درست بودن خودکار تمام تنظیمات تست نیست. Environment، pluginها و مسیر فایل‌ها باید با نیاز تست سازگار باشند. سرعت هم به تعداد تست، نوع محیط و کار setup وابسته است. جملهٔ همیشه سریع‌تر از Jest، بدون پروژه و روش مقایسه، اطلاعات قابل استفاده‌ای نمی‌دهد. راهنمای شروع Vitest روش نصب و اجرای فعلی را بیان می‌کند.

از رفتار کوچک و قابل مشاهده شروع کنید

برای شروع، تابعی انتخاب کنید که ورودی و خروجی روشن دارد. Test باید یک قرارداد را با مقدار قابل فهم بیان کند؛ مثلاً تخفیف ۲۵ درصد از دو کالای ۱۲۰۰ واحدی، جمع ۱۸۰۰ می‌دهد. این تست به شکل داخلی تابع وابسته نیست. اگر فردا محاسبه بازنویسی شود و قرارداد ثابت بماند، تست همچنان مفید است. تستی که فقط همان فرمول پیاده‌سازی را دوباره حساب می‌کند، ممکن است همان خطا را در هر دو طرف داشته باشد.

نام تست باید بگوید چه رفتاری انتظار می‌رود. مجموعهٔ describe برای گروه مرتبط مناسب است، اما لایه‌های تو در توی زیاد خواندن شکست را سخت می‌کنند. از ورودی مرزی، نمونهٔ معتبر و نمونهٔ نامعتبر استفاده کنید. تست‌های متعدد با همان نوع ورودی، به تنهایی پوشش تصمیم‌ها را کامل نمی‌کنند. ابتدا سیاست را با نویسندهٔ محصول روشن کنید و سپس انتظار عددی را ثبت کنید. ابزار تست نباید جای تعیین سیاست گرد کردن یا اعتبار داده را بگیرد.

Node، محیط شبیه‌سازی DOM و مرورگر فرق دارند

تست تابع قیمت به DOM احتیاج ندارد و environment برابر node مناسب است. برای کدی که از document استفاده می‌کند، محیط دیگری لازم می‌شود. محیط شبیه‌سازی DOM برخی APIها را فراهم می‌کند، اما rendering، layout و رفتار کامل مرورگر را ثابت نمی‌کند. Browser Mode مسیر اجرای تست در مرورگر واقعی را فراهم می‌کند و به provider و تنظیم خودش نیاز دارد. راهنمای Browser Mode این قرارداد را توضیح می‌دهد. انتخاب محیط باید از رفتار مورد بررسی بیاید.

کدی که در import اولیه به window دسترسی می‌زند، ممکن است حتی پیش از شروع تست در Node شکست بخورد. جدا کردن منطق خالص از adapter رابط، هم تست را ساده‌تر می‌کند و هم مسئولیت را روشن‌تر. تابع قیمت نباید برای محاسبه به دکمهٔ صفحه وابسته باشد. برای تعامل کاربر، تست سطح رابط لازم است؛ برای مبلغ، تست تابع دامنه مفید است. این دو سطح، شواهد متفاوتی می‌دهند و نباید نتیجهٔ یکی را به دیگری تعمیم داد.

نمونهٔ کامل برای قرارداد تخفیف

نمونه، قیمت را در واحد صحیح پولی می‌گیرد؛ درصد تخفیف هم عدد صحیح است. سیاست گرد کردن، یک بار روی جمع نهایی انجام می‌شود. این قرارداد فرضی آموزش است و برای حسابداری واقعی باید با نیاز محصول تطبیق داده شود. Node 24 و npm لازم‌اند. package.json و vitest.config.js را در ریشه، و price.js و price.test.js را داخل src ذخیره کنید. نام فایل‌های نمونه را هنگام کپی به همین نام‌های مقصد تبدیل کنید. این نمونه رابط وب ندارد و بنابراین فایل HTML و CSS لازم ندارد.

{"name":"vitest-discount-demo","private":true,"version":"1.0.0","type":"module","scripts":{"test":"vitest","test:run":"vitest run"},"devDependencies":{"vitest":"^5.0.0","vite":"^8.0.0"}}
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { environment: 'node', include: ['src/**/*.test.js'] } });
export function discountedTotal(unitPrice, quantity, percent = 0) {
  if (!Number.isSafeInteger(unitPrice) || unitPrice < 0) throw new RangeError('Invalid price');
  if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 100) throw new RangeError('Invalid quantity');
  if (!Number.isInteger(percent) || percent < 0 || percent > 100) throw new RangeError('Invalid discount');
  const subtotal = unitPrice * quantity;
  if (!Number.isSafeInteger(subtotal)) throw new RangeError('Unsafe total');
  const discounted = subtotal * (100 - percent);
  if (!Number.isSafeInteger(discounted)) throw new RangeError('Unsafe calculation');
  return Math.round(discounted / 100);
}
import { describe, expect, it } from 'vitest';
import { discountedTotal } from './price.js';
describe('discountedTotal', () => {
  it('keeps total without discount', () => expect(discountedTotal(1200, 3)).toBe(3600));
  it('applies an integer percentage', () => expect(discountedTotal(1200, 2, 25)).toBe(1800));
  it('rounds the final total once', () => expect(discountedTotal(101, 3, 10)).toBe(273));
  it('allows free item and full discount', () => {
    expect(discountedTotal(0, 1, 20)).toBe(0);
    expect(discountedTotal(1000, 2, 100)).toBe(0);
  });
  it.each([0, -1, 1.5, 101, '2', NaN])('rejects quantity %s', (quantity) => {
    expect(() => discountedTotal(1200, quantity)).toThrow(RangeError);
  });
  it('rejects invalid price and discount', () => {
    expect(() => discountedTotal(-10, 1)).toThrow(RangeError);
    expect(() => discountedTotal(1000, 1, 101)).toThrow(RangeError);
  });
  it('rejects unsafe intermediate multiplication', () => {
    expect(() => discountedTotal(Number.MAX_SAFE_INTEGER, 1, 0)).toThrow(RangeError);
  });
});

npm install وابستگی‌ها و lock واقعی می‌سازد. npm test حالت watch را شروع می‌کند و برای تغییر محلی مناسب است. npm run test:run یک اجرای پایان‌پذیر دارد و برای CI مناسب‌تر است. فایل lock را همراه package نگه دارید و در نصب تازه از npm ci استفاده کنید. محدودهٔ نسخهٔ اصلی در مثال مشخص است و ادعای استفاده از جدیدترین نسخه در هر زمان ندارد. این مقاله نمونه و روش اجرا را ارائه می‌دهد؛ نتیجهٔ اجرای تست در محیطی مشخص گزارش نشده است.

تابع ابتدا نوع و محدودهٔ ورودی را بررسی می‌کند. ضرب‌های میانی هم باید در محدودهٔ integer امن JavaScript بمانند. حتی اگر مقدار نهایی کوچک‌تر شود، محاسبهٔ میانی نامطمئن می‌تواند دقت را از دست بدهد. تست آخر این سیاست محافظه‌کارانه را ثبت می‌کند. برای مبلغ‌های بزرگ یا محاسبات مالی حساس، مدل عددی مناسب دیگری ممکن است لازم شود. استفاده از عدد صحیح در این نمونه، تضمین دقت برای همهٔ مقدارهای ممکن ایجاد نمی‌کند.

Assertion دربارهٔ نتیجه باشد

expect با toBe در این مثال مقدار ساده را مقایسه می‌کند. برای object و array، مقایسهٔ محتوایی مناسب دیگری لازم است؛ برابر بودن reference با برابر بودن داده یکی نیست. تست خطا باید تابعی را به assertion بدهد تا پرتاب خطا در محدودهٔ بررسی انجام شود. اگر تابع را پیش از assertion اجرا کنید، ممکن است خطا به شکلی نامرتبط تست را قطع کند. پیام شکست خوب باید ورودی و قرارداد مورد انتظار را قابل شناسایی نگه دارد، نه اینکه فقط بگوید چیزی کار نکرد.

برای Promise باید پایان عملیات را منتظر بمانید یا assertion غیرهم‌زمان را برگردانید. تمام شدن بدنهٔ تست پیش از پایان Promise می‌تواند شاهد ناقص بدهد. زمان ثابت و انتظار تصادفی برای پایان عملیات هم شکننده‌اند. مقالهٔ Promise و async/await ترتیب، خطا و لغو را توضیح می‌دهد. هر تست غیرهم‌زمان باید معلوم کند چه رویدادی پایان رفتار مورد انتظار است و خطا کجا مشاهده می‌شود.

Mock مرز بیرونی را جایگزین می‌کند

اگر تابع به ساعت، شبکه یا یک سرویس بیرونی وابسته است، mock یا تزریق وابستگی می‌تواند تست را محدود و تکرارپذیر کند. اما mock کردن همهٔ تابع‌های داخلی باعث می‌شود تست بیشتر شکل پیاده‌سازی را بررسی کند. وقتی رفتار واقعی dependency عوض شد، mock قدیمی ممکن است همچنان سبز باشد. راهنمای mocking ابزارها و محدودیت‌ها را توضیح می‌دهد. نمونهٔ قیمت خالص است و نیاز به mock ندارد؛ اضافه کردن mock فقط برای نمایش API، قرارداد را بهتر نمی‌کند.

Timer جعلی باید پس از تست به وضعیت مناسب برگردد تا تست بعدی رفتار دیگری نگیرد. همین موضوع برای spy و mockهای مشترک صادق است. تست‌هایی که تنها به ترتیب اجرای خاصی موفق می‌شوند، وابستگی پنهان دارند. Setup مشترک را کوتاه و قابل خواندن نگه دارید و state قابل تغییر را میان تست‌ها بدون دلیل به اشتراک نگذارید. اگر یک failure فقط در مجموعهٔ کامل دیده می‌شود، آلودگی state میان تست‌ها یکی از احتمال‌های قابل بررسی است.

Coverage شمارندهٔ اجرای کد است

Coverage نشان می‌دهد کدام خط‌ها، شاخه‌ها یا تابع‌ها در اجرای تست دیده شده‌اند. اجرای یک خط به معنی بررسی درست نتیجهٔ آن نیست. می‌توان coverage بالا داشت و assertion ضعیف نوشت. برای جمع‌آوری گزارش، provider مربوط و نسخهٔ سازگار آن لازم است؛ صرف افزودن یک flag همیشه تمام dependencyها را فراهم نمی‌کند. راهنمای coverage گزینه‌های موجود را توضیح می‌دهد. نمونهٔ کوتاه ما provider ندارد و گزارش coverage ادعا نمی‌کند.

اگر هدف عددی برای coverage تعیین می‌کنید، آن را کنار نوع ریسک بخوانید. یک شرط خطای پرداخت ممکن است از ده‌ها خط نمایش متن مهم‌تر باشد. Snapshot بزرگ هم می‌تواند تغییرهای غیرضروری زیادی نشان دهد؛ تأیید مکانیکی snapshot بدون خواندن تفاوت، شاهد مفیدی نیست. برای دادهٔ کوچک و پایدار snapshot ممکن است مناسب باشد، ولی قرارداد مشخص عددی در این مثال مستقیم‌تر است. نوع assertion باید به تصمیمی که می‌خواهید حفظ کنید وصل شود.

تست را به مسیر تحویل وصل کنید

در CI حالت watch مناسب نیست؛ job باید بعد از بررسی نتیجه پایان یابد. خروجی شکست و کد خروج باید merge یا مرحلهٔ بعد را مطابق سیاست تیم متوقف کنند. نوشتهٔ CI/CD برای فرانت‌اند نمونهٔ workflow و فرق build با تست را بیان می‌کند. اگر تست محلی سبز و CI قرمز است، نسخهٔ Node، lock، متغیرهای محیطی و وابستگی به فایل یا زمان را مقایسه کنید. افزایش retry بدون تشخیص علت، فقط شکست را کمتر قابل فهم می‌کند.

برای نگهداری این مجموعه، هر بار سیاست قیمت تغییر کرد ابتدا انتظار را با مثال عددی روشن کنید. تغییر test برای سبز شدن کد موجود باید دلیل رفتاری داشته باشد. در پروژهٔ React، مالکیت state را هم جدا طراحی کنید؛ مقالهٔ Zustand و state مشترک این مرز را توضیح می‌دهد. تست مناسب، تصمیمی را که قرار است باقی بماند قابل خواندن می‌کند و به تیم اجازه می‌دهد جزئیات اجرا را با اطمینان بیشتری تغییر دهد.

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

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

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

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