Vitest چیست؟ آموزش کامل تست JavaScript، TypeScript و React

در این آموزش Vitest را از صفر یاد می‌گیرید؛ از نصب و نوشتن Unit Test تا Mock کردن API، تست کامپوننت‌های React، Coverage و اجرای خودکار تست‌ها در CI. همچنین سرویس هوش مصنوعی را بدون مصرف واقعی API تست می‌کنیم.

Share
Vitest چیست؟ آموزش کامل تست JavaScript، TypeScript و React


تست‌نویسی یکی از مهم‌ترین مهارت‌های توسعه نرم‌افزار است؛ اما انتخاب ابزار نامناسب، تنظیمات پیچیده و تست‌های کند می‌تواند برنامه‌نویسان را از انجام آن منصرف کند. Vitest یک فریم‌ورک مدرن برای تست پروژه‌های JavaScript و TypeScript است که به‌خصوص در برنامه‌های ساخته‌شده با Vite، React، Vue و سایر ابزارهای مدرن فرانت‌اند عملکرد بسیار خوبی دارد.

Vitest از پیکربندی، افزونه‌ها، Resolverها و فرایند تبدیل کد در Vite استفاده می‌کند. در نتیجه، معمولاً لازم نیست برای اجرای تست‌ها یک زنجیره Build جداگانه بسازید. پشتیبانی مستقیم از TypeScript، JSX، ESM، تست‌های Async، Mock، Snapshot و Code Coverage نیز باعث شده است Vitest برای پروژه‌های جدید انتخاب مناسبی باشد.

در این آموزش، Vitest را از نصب اولیه تا تست کامپوننت React، Mock کردن درخواست API، تست سرویس هوش مصنوعی و اجرای خودکار تست‌ها در CI بررسی می‌کنیم.

Vitest چیست؟

Vitest یک Test Runner یا اجراکننده تست برای اکوسیستم JavaScript و TypeScript است که بر پایه Vite ساخته شده است. این ابزار فایل‌های تست را پیدا می‌کند، کد پروژه را اجرا می‌کند، نتیجه مورد انتظار را با نتیجه واقعی مقایسه می‌کند و گزارشی از تست‌های موفق و ناموفق ارائه می‌دهد.

بر اساس مستندات رسمی Vitest، این ابزار به‌صورت مستقیم از TypeScript و JSX پشتیبانی می‌کند و می‌تواند تنظیمات Vite موجود در پروژه را بخواند.

یک تست ساده با Vitest چنین ساختاری دارد:

import { expect, test } from 'vitest'

function sum(a: number, b: number) {
  return a + b
}

test('دو عدد را با یکدیگر جمع می‌کند', () => {
  expect(sum(2, 3)).toBe(5)
})

در این مثال:

  • test یک سناریوی قابل‌آزمایش تعریف می‌کند.
  • expect نتیجه واقعی را دریافت می‌کند.
  • toBe نتیجه واقعی را با مقدار مورد انتظار مقایسه می‌کند.

اگر تابع sum مقدار 5 را برگرداند، تست موفق می‌شود. اگر خروجی متفاوت باشد، Vitest تست را شکست‌خورده اعلام کرده و تفاوت نتیجه واقعی و مورد انتظار را نمایش می‌دهد.

چرا باید از Vitest استفاده کنیم؟

مزیت Vitest فقط سرعت اجرای تست نیست. مهم‌ترین مزایای آن عبارت‌اند از:

  • هماهنگی مستقیم با Vite
  • پشتیبانی از JavaScript، TypeScript و JSX
  • پشتیبانی مناسب از ESM
  • Watch Mode سریع هنگام توسعه
  • API آشنا برای کاربران Jest
  • قابلیت Mock کردن تابع، ماژول، زمان و متغیرهای سراسری
  • پشتیبانی از Snapshot Testing
  • تولید گزارش Code Coverage
  • امکان تست کامپوننت‌های React، Vue و سایر فریم‌ورک‌ها
  • قابلیت اجرای تست در محیط Node، jsdom، happy-dom یا مرورگر واقعی
  • مناسب برای اجرای خودکار در CI/CD

Vitest به‌صورت پیش‌فرض فایل‌هایی را که عبارت .test. یا .spec. در نام آن‌ها وجود دارد به‌عنوان فایل تست شناسایی می‌کند. برای مثال:

sum.test.ts
user.service.spec.ts
LoginForm.test.tsx

تفاوت Unit Test، Integration Test و End-to-End Test

قبل از نصب Vitest باید بدانیم قرار است چه چیزی را آزمایش کنیم.

Unit Test یا تست واحد

در Unit Test کوچک‌ترین بخش‌های مستقل برنامه، مانند تابع، کلاس یا ماژول، آزمایش می‌شوند.

مثال:

export function calculateDiscount(
  price: number,
  percent: number
): number {
  return price - price * (percent / 100)
}

تست این تابع:

import { describe, expect, it } from 'vitest'
import { calculateDiscount } from './calculateDiscount'

describe('calculateDiscount', () => {
  it('تخفیف را به‌درستی محاسبه می‌کند', () => {
    expect(calculateDiscount(1_000_000, 20)).toBe(800_000)
  })
})

Integration Test یا تست یکپارچگی

در این نوع تست، ارتباط چند بخش از برنامه بررسی می‌شود؛ مثلاً ارتباط سرویس کاربر با Repository یا عملکرد یک فرم React همراه با اعتبارسنجی و ارسال اطلاعات.

End-to-End Test یا تست سرتاسری

در تست E2E، رفتار کامل برنامه از دید کاربر بررسی می‌شود. برای مثال، کاربر وارد صفحه ورود می‌شود، اطلاعات خود را می‌نویسد، روی دکمه ورود کلیک می‌کند و به داشبورد منتقل می‌شود.

Vitest بیشتر برای Unit Test و Integration Test استفاده می‌شود. برای تست کامل مرورگر، ابزارهایی مانند Playwright انتخاب رایج‌تری هستند.

مقایسه Vitest با Jest و Playwright

ابزارکاربرد اصلیمحیط اجرامناسب برای
VitestUnit و Integration TestNode، DOM شبیه‌سازی‌شده یا Browser Modeپروژه‌های Vite، React، Vue و TypeScript
JestUnit و Integration TestNode و DOM شبیه‌سازی‌شدهپروژه‌های قدیمی‌تر و اکوسیستم گسترده Jest
PlaywrightEnd-to-End Testمرورگر واقعیتست جریان کامل کاربر و رابط کاربری
Testing Libraryتست رفتار رابط کاربریهمراه با Test Runnerتست کامپوننت‌های React و سایر فریم‌ورک‌ها

Vitest جایگزین مستقیم Playwright نیست. در یک پروژه حرفه‌ای می‌توان از Vitest برای تست منطق و کامپوننت‌ها و از Playwright برای تست مسیرهای اصلی کاربر استفاده کرد.

پیش‌نیازهای نصب Vitest

نسخه جاری Vitest به Node.js نسخه ۲۰ یا جدیدتر و Vite نسخه ۶ یا جدیدتر نیاز دارد. بهتر است قبل از نصب، نسخه Node.js را بررسی کنید:

node --version
npm --version

اگر پروژه جدیدی می‌سازید، می‌توانید از Vite استفاده کنید:

npm create vite@latest vitest-demo
cd vitest-demo
npm install

در زمان ساخت پروژه، می‌توانید React و TypeScript را انتخاب کنید.

نصب Vitest

برای نصب Vitest دستور زیر را اجرا کنید:

npm install --save-dev vitest

سپس اسکریپت‌های تست را به package.json اضافه کنید:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

تفاوت دو دستور اصلی چنین است:

npm run test

Vitest را در Watch Mode اجرا می‌کند. با تغییر فایل‌ها، تست‌های مرتبط دوباره اجرا می‌شوند.

npm run test:run

تست‌ها را فقط یک بار اجرا می‌کند. این حالت برای CI مناسب‌تر است.

اولین تست با Vitest

فایل زیر را ایجاد کنید:

// src/utils/formatPrice.ts

export function formatPrice(value: number): string {
  if (!Number.isFinite(value)) {
    throw new Error('Price must be a finite number')
  }

  return new Intl.NumberFormat('fa-IR').format(value)
}

فایل تست:

// src/utils/formatPrice.test.ts

import { describe, expect, it } from 'vitest'
import { formatPrice } from './formatPrice'

describe('formatPrice', () => {
  it('عدد را با قالب فارسی نمایش می‌دهد', () => {
    expect(formatPrice(1250000)).toBe('۱٬۲۵۰٬۰۰۰')
  })

  it('عدد صفر را پشتیبانی می‌کند', () => {
    expect(formatPrice(0)).toBe('۰')
  })

  it('برای مقدار نامعتبر خطا ایجاد می‌کند', () => {
    expect(() => formatPrice(Number.NaN)).toThrow(
      'Price must be a finite number'
    )
  })
})

اکنون تست را اجرا کنید:

npm run test

ساختار describe، it و expect

در Vitest معمولاً تست‌ها را با describe گروه‌بندی می‌کنیم:

describe('نام ماژول یا قابلیت', () => {
  it('رفتار مورد انتظار را توضیح می‌دهد', () => {
    expect(actualValue).toBe(expectedValue)
  })
})

test و it از نظر عملکرد تقریباً معادل‌اند:

test('جمع را محاسبه می‌کند', () => {
  expect(2 + 3).toBe(5)
})

it('جمع را محاسبه می‌کند', () => {
  expect(2 + 3).toBe(5)
})

استفاده از نام تست دقیق اهمیت زیادی دارد. این نام مناسب نیست:

it('works', () => {
  // ...
})

نام بهتر:

it('برای کاربر بدون ایمیل خطای اعتبارسنجی برمی‌گرداند', () => {
  // ...
})

Matcherهای پرکاربرد در Vitest

Matcher مشخص می‌کند نتیجه چگونه ارزیابی شود.

مقایسه مقادیر ساده

expect(10).toBe(10)
expect('hello').toBe('hello')
expect(true).toBeTruthy()
expect(false).toBeFalsy()

مقایسه Object و Array

برای Object و Array معمولاً از toEqual استفاده کنید:

const user = {
  id: 1,
  name: 'Sara'
}

expect(user).toEqual({
  id: 1,
  name: 'Sara'
})

بررسی بخشی از Object:

expect(user).toMatchObject({
  name: 'Sara'
})

بررسی Array:

expect(['React', 'TypeScript']).toContain('React')

بررسی عدد

expect(20).toBeGreaterThan(10)
expect(5).toBeLessThanOrEqual(5)
expect(0.1 + 0.2).toBeCloseTo(0.3)

بررسی String

expect('Vitest tutorial').toContain('Vitest')
expect('user@example.com').toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)

بررسی خطا

function parseAge(value: number) {
  if (value < 0) {
    throw new Error('Age cannot be negative')
  }

  return value
}

expect(() => parseAge(-1)).toThrow('Age cannot be negative')

تست توابع Async

فرض کنید تابعی برای دریافت کاربر داریم:

export async function loadUser(id: number) {
  if (id <= 0) {
    throw new Error('Invalid user id')
  }

  return {
    id,
    name: 'Ali'
  }
}

تست موفق:

import { expect, it } from 'vitest'
import { loadUser } from './loadUser'

it('اطلاعات کاربر را برمی‌گرداند', async () => {
  await expect(loadUser(1)).resolves.toEqual({
    id: 1,
    name: 'Ali'
  })
})

تست خطا:

it('برای شناسه نامعتبر خطا می‌دهد', async () => {
  await expect(loadUser(0)).rejects.toThrow('Invalid user id')
})

یکی از اشتباهات رایج، فراموش کردن await است. اگر Promise را Await نکنید، ممکن است تست قبل از تکمیل عملیات تمام شود.

Setup و Teardown در تست‌ها

گاهی لازم است قبل یا بعد از هر تست کاری انجام شود:

import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  expect,
  it
} from 'vitest'

describe('database service', () => {
  beforeAll(() => {
    // یک بار قبل از تمام تست‌های این گروه
  })

  beforeEach(() => {
    // قبل از هر تست
  })

  afterEach(() => {
    // بعد از هر تست
  })

  afterAll(() => {
    // یک بار بعد از تمام تست‌ها
  })

  it('نمونه تست', () => {
    expect(true).toBe(true)
  })
})

از beforeEach برای بازگرداندن وضعیت تست، ساخت داده موقت یا پاک‌سازی Mockها استفاده می‌شود. تست‌ها نباید به ترتیب اجرای یکدیگر وابسته باشند.

Mock چیست؟

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

Vitest ابزارهای Mock را از طریق شیء vi ارائه می‌کند. مستندات رسمی توصیه می‌کند وضعیت Mockها میان تست‌ها پاک یا بازیابی شود تا تست‌ها روی یکدیگر اثر نگذارند. جزئیات بیشتر در راهنمای Mocking در Vitest موجود است.

ساخت Mock Function با vi.fn

import { expect, it, vi } from 'vitest'

it('تابع callback را فراخوانی می‌کند', () => {
  const onSuccess = vi.fn()

  onSuccess({
    id: 10,
    status: 'completed'
  })

  expect(onSuccess).toHaveBeenCalledOnce()
  expect(onSuccess).toHaveBeenCalledWith({
    id: 10,
    status: 'completed'
  })
})

می‌توان خروجی تابع Mock را نیز تعیین کرد:

const getUser = vi.fn().mockReturnValue({
  id: 1,
  name: 'Nima'
})

expect(getUser()).toEqual({
  id: 1,
  name: 'Nima'
})

برای تابع Async:

const getUser = vi.fn().mockResolvedValue({
  id: 1,
  name: 'Nima'
})

برای شبیه‌سازی خطا:

const getUser = vi.fn().mockRejectedValue(
  new Error('Service unavailable')
)

استفاده از vi.spyOn

vi.spyOn برای بررسی فراخوانی یک متد موجود مناسب است:

import { expect, it, vi } from 'vitest'

it('هشدار را ثبت می‌کند', () => {
  const spy = vi
    .spyOn(console, 'warn')
    .mockImplementation(() => undefined)

  console.warn('Invalid response')

  expect(spy).toHaveBeenCalledWith('Invalid response')

  spy.mockRestore()
})

mockRestore پیاده‌سازی اصلی تابع را بازمی‌گرداند.

تفاوت clear، reset و restore

این سه عملیات یکسان نیستند:

  • clearAllMocks تاریخچه فراخوانی‌ها را پاک می‌کند.
  • resetAllMocks تاریخچه و پیاده‌سازی Mock را بازنشانی می‌کند.
  • restoreAllMocks توابع Spy شده را به پیاده‌سازی اصلی برمی‌گرداند.

یک فایل Setup مناسب می‌تواند چنین باشد:

import { afterEach, vi } from 'vitest'

afterEach(() => {
  vi.restoreAllMocks()
  vi.unstubAllGlobals()
  vi.useRealTimers()
})

تست API بدون ارسال درخواست واقعی

فرض کنید در بخش سرور برنامه، سرویس زیر را برای ارتباط با API هوش مصنوعی نوشته‌ایم:

// src/server/generateAnswer.ts

type GenerateAnswerOptions = {
  prompt: string
  apiKey: string
  model: string
}

type ChatResponse = {
  choices: Array<{
    message: {
      content: string
    }
  }>
}

export async function generateAnswer({
  prompt,
  apiKey,
  model
}: GenerateAnswerOptions): Promise<string> {
  const response = await fetch(
    'https://api.darvareh.ir/v1/chat/completions',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model,
        messages: [
          {
            role: 'user',
            content: prompt
          }
        ]
      })
    }
  )

  if (!response.ok) {
    throw new Error(`AI request failed with status ${response.status}`)
  }

  const data = (await response.json()) as ChatResponse
  const content = data.choices[0]?.message?.content

  if (!content) {
    throw new Error('AI response does not contain content')
  }

  return content
}

کلید API نباید در کد فرانت‌اند، Repository عمومی یا فایل‌های قابل‌دسترسی مرورگر قرار گیرد. این تابع باید در Backend اجرا شود و کلید از Environment Variable دریافت شود.

برای آزمایش این سرویس لازم نیست درخواست واقعی ارسال کنیم. می‌توان fetch را Mock کرد:

// src/server/generateAnswer.test.ts

import {
  afterEach,
  describe,
  expect,
  it,
  vi
} from 'vitest'

import { generateAnswer } from './generateAnswer'

describe('generateAnswer', () => {
  afterEach(() => {
    vi.unstubAllGlobals()
  })

  it('پاسخ مدل را استخراج می‌کند', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: async () => ({
        choices: [
          {
            message: {
              content: 'این یک پاسخ آزمایشی است.'
            }
          }
        ]
      })
    })

    vi.stubGlobal('fetch', fetchMock)

    const result = await generateAnswer({
      prompt: 'یک متن کوتاه بنویس',
      apiKey: 'YOUR_DARVAREH_API_KEY',
      model: 'YOUR_MODEL_ID'
    })

    expect(result).toBe('این یک پاسخ آزمایشی است.')

    expect(fetchMock).toHaveBeenCalledOnce()
    expect(fetchMock).toHaveBeenCalledWith(
      'https://api.darvareh.ir/v1/chat/completions',
      expect.objectContaining({
        method: 'POST',
        headers: {
          Authorization: 'Bearer YOUR_DARVAREH_API_KEY',
          'Content-Type': 'application/json'
        }
      })
    )
  })

  it('خطای HTTP را مدیریت می‌کند', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 429
      })
    )

    await expect(
      generateAnswer({
        prompt: 'سلام',
        apiKey: 'YOUR_DARVAREH_API_KEY',
        model: 'YOUR_MODEL_ID'
      })
    ).rejects.toThrow('AI request failed with status 429')
  })

  it('پاسخ بدون content را رد می‌کند', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        json: async () => ({
          choices: []
        })
      })
    )

    await expect(
      generateAnswer({
        prompt: 'سلام',
        apiKey: 'YOUR_DARVAREH_API_KEY',
        model: 'YOUR_MODEL_ID'
      })
    ).rejects.toThrow('AI response does not contain content')
  })
})

این تست‌ها:

  • هیچ درخواست اینترنتی واقعی ارسال نمی‌کنند.
  • هیچ اعتبار یا توکنی مصرف نمی‌کنند.
  • حالت موفق، خطای HTTP و پاسخ ناقص را بررسی می‌کنند.
  • محتوای Header و URL درخواست را کنترل می‌کنند.
  • بدون استفاده از کلید واقعی API اجرا می‌شوند.

برای مشاهده مدل‌های قابل‌استفاده و Model ID درواره می‌توانید به صفحه مدل‌های درواره مراجعه کنید.

چرا Mock کردن API در تست اهمیت دارد؟

اگر Unit Test به یک API واقعی وابسته باشد، نتیجه آن ممکن است تحت‌تأثیر عوامل زیر قرار گیرد:

  • قطع یا کندی اینترنت
  • محدودیت تعداد درخواست
  • تغییر پاسخ سرویس
  • افزایش هزینه مصرف API
  • خطاهای موقت شبکه
  • تغییر داده‌های خارجی
  • افشای ناخواسته کلید API در محیط CI

Unit Test باید سریع، تکرارپذیر و قابل‌پیش‌بینی باشد. با این حال، علاوه بر Unit Test می‌توان تعداد محدودی Integration Test جداگانه برای بررسی ارتباط واقعی با محیط آزمایشی سرویس تعریف کرد. این تست‌ها بهتر است فقط در فرایند مشخص CI و با Secretهای محافظت‌شده اجرا شوند.

تست کامپوننت React با Vitest

برای تست کامپوننت‌های React می‌توان از React Testing Library استفاده کرد. فلسفه Testing Library این است که رابط کاربری تا حد امکان مشابه رفتار واقعی کاربر آزمایش شود. برای اطلاعات بیشتر می‌توانید مستندات رسمی React Testing Library را ببینید.

بسته‌های موردنیاز را نصب کنید:

npm install --save-dev \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/user-event \
  jsdom

فایل پیکربندی Vitest:

// vitest.config.ts

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    clearMocks: true,
    restoreMocks: true
  }
})

فایل Setup:

// src/test/setup.ts

import '@testing-library/jest-dom/vitest'

اکنون یک کامپوننت ساده می‌سازیم:

// src/components/PromptForm.tsx

import { FormEvent, useState } from 'react'

type PromptFormProps = {
  onSubmit: (prompt: string) => Promise<void> | void
}

export function PromptForm({ onSubmit }: PromptFormProps) {
  const [prompt, setPrompt] = useState('')
  const [error, setError] = useState('')

  async function handleSubmit(event: FormEvent) {
    event.preventDefault()

    const normalizedPrompt = prompt.trim()

    if (!normalizedPrompt) {
      setError('متن درخواست را وارد کنید')
      return
    }

    setError('')
    await onSubmit(normalizedPrompt)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="prompt">درخواست شما</label>

      <textarea
        id="prompt"
        value={prompt}
        onChange={(event) => setPrompt(event.target.value)}
      />

      {error ? <p role="alert">{error}</p> : null}

      <button type="submit">ارسال</button>
    </form>
  )
}

تست کامپوننت:

// src/components/PromptForm.test.tsx

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'

import { PromptForm } from './PromptForm'

describe('PromptForm', () => {
  it('برای متن خالی پیام خطا نمایش می‌دهد', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()

    render(<PromptForm onSubmit={onSubmit} />)

    await user.click(
      screen.getByRole('button', {
        name: 'ارسال'
      })
    )

    expect(
      screen.getByRole('alert')
    ).toHaveTextContent('متن درخواست را وارد کنید')

    expect(onSubmit).not.toHaveBeenCalled()
  })

  it('متن کاربر را ارسال می‌کند', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()

    render(<PromptForm onSubmit={onSubmit} />)

    await user.type(
      screen.getByLabelText('درخواست شما'),
      'یک عنوان برای مقاله پیشنهاد بده'
    )

    await user.click(
      screen.getByRole('button', {
        name: 'ارسال'
      })
    )

    expect(onSubmit).toHaveBeenCalledOnce()
    expect(onSubmit).toHaveBeenCalledWith(
      'یک عنوان برای مقاله پیشنهاد بده'
    )
  })
})

در این تست به‌جای بررسی State داخلی React، رفتار قابل‌مشاهده کاربر بررسی شده است:

  • کاربر روی دکمه کلیک می‌کند.
  • پیام خطا را مشاهده می‌کند.
  • متن موردنظر را وارد می‌کند.
  • Callback با مقدار مناسب فراخوانی می‌شود.

این نوع تست در برابر تغییرات داخلی کامپوننت مقاوم‌تر است.

انتخاب Query مناسب در Testing Library

ترتیب پیشنهادی برای یافتن عناصر معمولاً چنین است:

  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByTestId

برای مثال، این انتخاب به رفتار کاربر و دسترس‌پذیری نزدیک است:

screen.getByRole('button', {
  name: 'ارسال'
})

استفاده بیش‌ازحد از data-testid می‌تواند تست را به ساختار داخلی HTML وابسته کند. data-testid زمانی مناسب است که عنصر هیچ نقش، Label یا متن قابل‌استفاده‌ای نداشته باشد.

تفاوت getBy، queryBy و findBy

getBy

اگر عنصر وجود نداشته باشد، بلافاصله خطا ایجاد می‌کند:

screen.getByRole('button')

queryBy

اگر عنصر وجود نداشته باشد، null برمی‌گرداند. برای بررسی حذف یا نبود عنصر مناسب است:

expect(
  screen.queryByText('خطا')
).not.toBeInTheDocument()

findBy

یک Promise برمی‌گرداند و برای عناصری مناسب است که به‌صورت Async ظاهر می‌شوند:

expect(
  await screen.findByText('پاسخ دریافت شد')
).toBeInTheDocument()

تست کامپوننتی که پاسخ Async نمایش می‌دهد

import { useState } from 'react'

type AnswerBoxProps = {
  loadAnswer: () => Promise<string>
}

export function AnswerBox({ loadAnswer }: AnswerBoxProps) {
  const [answer, setAnswer] = useState('')
  const [loading, setLoading] = useState(false)

  async function handleClick() {
    setLoading(true)

    try {
      const result = await loadAnswer()
      setAnswer(result)
    } finally {
      setLoading(false)
    }
  }

  return (
    <section>
      <button onClick={handleClick}>
        دریافت پاسخ
      </button>

      {loading ? <p>در حال دریافت...</p> : null}
      {answer ? <p>{answer}</p> : null}
    </section>
  )
}

تست:

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { expect, it, vi } from 'vitest'

import { AnswerBox } from './AnswerBox'

it('پاسخ Async را نمایش می‌دهد', async () => {
  const user = userEvent.setup()

  const loadAnswer = vi
    .fn()
    .mockResolvedValue('پاسخ آزمایشی مدل')

  render(<AnswerBox loadAnswer={loadAnswer} />)

  await user.click(
    screen.getByRole('button', {
      name: 'دریافت پاسخ'
    })
  )

  expect(
    await screen.findByText('پاسخ آزمایشی مدل')
  ).toBeInTheDocument()

  expect(loadAnswer).toHaveBeenCalledOnce()
})

تست Timer و تأخیر

برای تست منطق وابسته به زمان می‌توان از Fake Timer استفاده کرد:

import {
  afterEach,
  expect,
  it,
  vi
} from 'vitest'

function scheduleRetry(callback: () => void) {
  setTimeout(callback, 3000)
}

afterEach(() => {
  vi.useRealTimers()
})

it('پس از سه ثانیه دوباره تلاش می‌کند', () => {
  vi.useFakeTimers()

  const callback = vi.fn()
  scheduleRetry(callback)

  expect(callback).not.toHaveBeenCalled()

  vi.advanceTimersByTime(3000)

  expect(callback).toHaveBeenCalledOnce()
})

Fake Timer باعث می‌شود تست واقعاً سه ثانیه منتظر نماند.

تست Snapshot چیست؟

Snapshot Testing خروجی یک تابع یا کامپوننت را ذخیره می‌کند و در اجرای بعدی با خروجی جدید مقایسه می‌کند. اگر خروجی تغییر کرده باشد، تست شکست می‌خورد.

import { expect, it } from 'vitest'

it('ساختار تنظیمات را تغییر نمی‌دهد', () => {
  const config = {
    model: 'YOUR_MODEL_ID',
    temperature: 0.2,
    stream: false
  }

  expect(config).toMatchSnapshot()
})

مطابق راهنمای رسمی Snapshot در Vitest، فایل Snapshot باید همراه کد Commit و تغییرات آن در Code Review بررسی شود.

Snapshot جایگزین Assertion دقیق نیست. Snapshotهای بسیار بزرگ معمولاً بدون بررسی به‌روزرسانی می‌شوند و ارزش تست را کاهش می‌دهند. برای داده‌های حساس یا خروجی‌هایی که مرتب تغییر می‌کنند نیز Snapshot انتخاب مناسبی نیست.

Code Coverage چیست؟

Code Coverage نشان می‌دهد چه مقدار از کد هنگام اجرای تست‌ها اجرا شده است. معیارهای متداول عبارت‌اند از:

  • Statements: چند درصد دستورها اجرا شده‌اند؟
  • Branches: چند درصد شاخه‌های شرطی بررسی شده‌اند؟
  • Functions: چند درصد توابع فراخوانی شده‌اند؟
  • Lines: چند درصد خطوط اجرا شده‌اند؟

برای فعال کردن Coverage مبتنی بر V8 بسته زیر را نصب کنید:

npm install --save-dev @vitest/coverage-v8

پیکربندی:

// vitest.config.ts

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json'],
      include: ['src/**/*.{ts,tsx}'],
      exclude: [
        'src/**/*.d.ts',
        'src/main.tsx',
        'src/test/**'
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80
      }
    }
  }
})

گزارش را تولید کنید:

npm run test:coverage

Vitest از Providerهای V8 و Istanbul پشتیبانی می‌کند. جزئیات تنظیم آن‌ها در مستندات Coverage و Vitest آمده است.

Coverage بالا به‌تنهایی تضمین نمی‌کند تست‌ها باکیفیت‌اند. ممکن است یک تست فقط تابع را اجرا کند، اما رفتار مهم آن را بررسی نکند. هدف اصلی باید پوشش سناریوهای مهم باشد، نه رسیدن مصنوعی به عدد ۱۰۰ درصد.

اجرای تست‌های مشخص

اجرای یک فایل:

npx vitest run src/utils/formatPrice.test.ts

اجرای تست‌های مرتبط با یک عبارت:

npx vitest run -t "خطای HTTP"

اجرای تست‌های فایل‌های تغییرکرده:

npx vitest related src/server/generateAnswer.ts --run

استفاده موقت از only:

it.only('فقط این تست اجرا می‌شود', () => {
  expect(true).toBe(true)
})

نادیده گرفتن موقت تست:

it.skip('این تست فعلاً اجرا نمی‌شود', () => {
  expect(true).toBe(false)
})

قبل از Commit مطمئن شوید only ناخواسته در کد باقی نمانده باشد.

اجرای Vitest با رابط کاربری

برای نصب رابط گرافیکی Vitest:

npm install --save-dev @vitest/ui

اجرا:

npx vitest --ui

رابط کاربری برای مشاهده ساختار Suiteها، نتیجه تست‌ها و خطاها مفید است؛ اما نباید جایگزین خروجی استاندارد قابل‌استفاده در CI شود.

اجرای تست در GitHub Actions

فایل زیر را ایجاد کنید:

# .github/workflows/test.yml

name: Test

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Type check
        run: npm run typecheck

      - name: Run tests
        run: npm run test:run

      - name: Generate coverage
        run: npm run test:coverage

      - name: Build application
        run: npm run build

اسکریپت Type Check:

{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}

در پروژه واقعی بهتر است مراحل زیر در هر Pull Request اجرا شوند:

  • Lint
  • Type Check
  • Unit Test
  • Integration Test
  • Build

آزمون E2E را نیز می‌توان در Job جداگانه اجرا کرد.

ساختار پیشنهادی فایل‌های تست

دو روش متداول وجود دارد.

قرار دادن تست کنار فایل اصلی

src/
  components/
    PromptForm.tsx
    PromptForm.test.tsx
  server/
    generateAnswer.ts
    generateAnswer.test.ts

این ساختار پیدا کردن تست مرتبط با هر فایل را آسان می‌کند.

استفاده از پوشه tests

src/
  components/
    PromptForm.tsx
  server/
    generateAnswer.ts

tests/
  components/
    PromptForm.test.tsx
  server/
    generateAnswer.test.ts

برای پروژه‌های کوچک و متوسط، قرار دادن تست کنار فایل معمولاً ساده‌تر است. تست‌های یکپارچگی یا E2E را می‌توان در پوشه مستقل نگه داشت.

چه چیزهایی را باید تست کنیم؟

اولویت تست‌نویسی بهتر است بر اساس ریسک و اهمیت قابلیت تعیین شود:

  • منطق محاسباتی
  • اعتبارسنجی ورودی
  • تبدیل و پاک‌سازی داده
  • مدیریت خطاهای API
  • رفتارهای اصلی فرم‌ها
  • سطح دسترسی و نمایش شرطی رابط کاربری
  • منطق Retry و Timeout
  • تبدیل پاسخ API به ساختار داخلی برنامه
  • پردازش پاسخ ناقص یا نامعتبر
  • مسیرهای اصلی کسب‌وکار

نیازی نیست جزئیات داخلی کتابخانه‌های دیگر را دوباره تست کنید. برای مثال، لازم نیست ثابت کنید Array.map در JavaScript درست کار می‌کند؛ باید رفتار منطق خودتان را بررسی کنید.

الگوی Arrange، Act و Assert

ساختار AAA خوانایی تست را افزایش می‌دهد:

it('هزینه را بر اساس تعداد توکن محاسبه می‌کند', () => {
  // Arrange
  const inputTokens = 1000
  const pricePerMillion = 2

  // Act
  const cost =
    (inputTokens / 1_000_000) * pricePerMillion

  // Assert
  expect(cost).toBeCloseTo(0.002)
})
  • Arrange: داده و وابستگی‌ها را آماده کنید.
  • Act: عملیات مورد آزمایش را اجرا کنید.
  • Assert: نتیجه را بررسی کنید.

اشتباهات رایج در تست‌نویسی با Vitest

تست کردن جزئیات پیاده‌سازی

اگر تست مستقیماً State داخلی، نام متد خصوصی یا ساختار دقیق DOM را بررسی کند، با هر Refactor ساده شکست می‌خورد. رفتار قابل‌مشاهده را آزمایش کنید.

Mock کردن بیش‌ازحد

اگر تمام اجزای سیستم Mock شوند، تست دیگر ارتباط واقعی بین آن‌ها را بررسی نمی‌کند. فقط وابستگی‌های کند، غیرقابل‌پیش‌بینی یا دارای Side Effect را Mock کنید.

وابستگی تست‌ها به یکدیگر

هر تست باید مستقل باشد. تست دوم نباید به داده‌ای وابسته باشد که تست اول ساخته است.

استفاده از کلید واقعی API در تست

کلید واقعی را در فایل تست، Snapshot، Log یا Repository قرار ندهید. برای Unit Test از Mock استفاده کنید و Secretهای CI را فقط برای تست‌های یکپارچگی کنترل‌شده نگه دارید.

استفاده نادرست از toBe

برای مقایسه Object و Array از toEqual استفاده کنید:

expect({ id: 1 }).toEqual({ id: 1 })

این کد معمولاً درست نیست:

expect({ id: 1 }).toBe({ id: 1 })

زیرا دو Object مستقل، Reference یکسان ندارند.

فراموش کردن await

it('داده را دریافت می‌کند', async () => {
  await expect(loadData()).resolves.toBeDefined()
})

بدون await ممکن است تست به‌اشتباه موفق اعلام شود یا نتیجه غیرقابل‌پیش‌بینی باشد.

به‌روزرسانی کورکورانه Snapshot

قبل از اجرای دستور به‌روزرسانی Snapshot، تغییرات را بررسی کنید. شکست Snapshot ممکن است نشانه یک Regression واقعی باشد.

تمرکز افراطی روی Coverage

عدد Coverage یک شاخص است، نه هدف نهایی. تستی که Assertion مؤثر ندارد حتی اگر کد را اجرا کند، ارزش چندانی ایجاد نمی‌کند.

خطاهای متداول Vitest و راه‌حل آن‌ها

خطای document is not defined

تست در محیط Node اجرا شده، اما کد به DOM نیاز دارد. محیط را روی jsdom قرار دهید:

export default defineConfig({
  test: {
    environment: 'jsdom'
  }
})

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

npm install --save-dev jsdom

شناخته نشدن toBeInTheDocument

فایل Setup را بررسی کنید:

import '@testing-library/jest-dom/vitest'

و آدرس آن را در تنظیمات قرار دهید:

setupFiles: ['./src/test/setup.ts']

Mock پس از هر تست باقی می‌ماند

از تنظیمات زیر استفاده کنید:

test: {
  clearMocks: true,
  restoreMocks: true,
  unstubGlobals: true,
  unstubEnvs: true
}

ماژول پیدا نمی‌شود

اگر از Alias استفاده می‌کنید، مطمئن شوید تنظیم Alias در Vite و TypeScript هماهنگ است:

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(
        new URL('./src', import.meta.url)
      )
    }
  }
})

تنظیم paths در tsconfig.json نیز باید با آن هماهنگ باشد.

تست در سیستم توسعه موفق و در CI ناموفق است

موارد زیر را بررسی کنید:

  • نسخه Node.js
  • Lockfile و استفاده از npm ci
  • تفاوت Environment Variableها
  • حساسیت نام فایل به حروف بزرگ و کوچک در Linux
  • وابستگی تست به منطقه زمانی
  • وابستگی تست به ترتیب اجرا
  • باقی ماندن Mock یا Timer میان تست‌ها
  • وجود فایل‌های تولیدشده محلی که Commit نشده‌اند

آیا باید در Vitest از Browser Mode استفاده کنیم؟

Vitest علاوه بر محیط شبیه‌سازی‌شده jsdom، از Browser Mode نیز پشتیبانی می‌کند. Browser Mode تست را در مرورگر واقعی اجرا می‌کند و برای قابلیت‌هایی که شبیه‌سازی DOM کافی نیست مفید است.

برای بسیاری از Unit Testها و تست‌های معمول React، jsdom سریع‌تر و ساده‌تر است. Browser Mode زمانی ارزشمندتر می‌شود که رفتار واقعی مرورگر، Layout، APIهای خاص Browser یا تعامل پیچیده رابط کاربری اهمیت داشته باشد. جزئیات این قابلیت در راهنمای رسمی Browser Mode ارائه شده است.

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

یک برنامه متصل به API هوش مصنوعی را می‌توان در چند سطح آزمایش کرد:

سطح اول: توابع خالص

مواردی مانند این‌ها را با Unit Test بررسی کنید:

  • پاک‌سازی Prompt
  • اعتبارسنجی ورودی
  • محاسبه تعداد تقریبی کاراکتر
  • ساخت آرایه Messages
  • تبدیل پاسخ API
  • نمایش خطا به زبان فارسی

سطح دوم: سرویس API

درخواست HTTP را Mock کرده و بررسی کنید:

  • URL درست است.
  • Method روی POST قرار دارد.
  • Headerها صحیح‌اند.
  • Model ID ارسال می‌شود.
  • پاسخ موفق پردازش می‌شود.
  • خطاهای ۴۰۰، ۴۰۱، ۴۲۹ و ۵۰۰ مدیریت می‌شوند.
  • پاسخ ناقص باعث خطای کنترل‌شده می‌شود.

سطح سوم: کامپوننت رابط کاربری

رفتار کاربر را بررسی کنید:

  • Prompt خالی ارسال نمی‌شود.
  • Loading نمایش داده می‌شود.
  • پاسخ در صفحه ظاهر می‌شود.
  • خطا قابل‌فهم است.
  • هنگام ارسال، دکمه در صورت نیاز غیرفعال می‌شود.

سطح چهارم: Integration Test محدود

Backend آزمایشی، اعتبارسنجی و سرویس داخلی را کنار هم تست کنید، اما درخواست شبکه خارجی را همچنان Mock نگه دارید.

سطح پنجم: End-to-End Test

تعداد محدودی مسیر حیاتی مانند ورود، ارسال Prompt و نمایش پاسخ را با Playwright آزمایش کنید. برای جلوگیری از هزینه و ناپایداری، پاسخ مدل را در بیشتر تست‌های E2E نیز می‌توان Stub کرد.

چک‌لیست تست پروژه پیش از انتشار

  • آیا تمام تست‌ها مستقل‌اند؟
  • آیا تست Async به‌درستی Await شده است؟
  • آیا Mockها پس از هر تست پاک می‌شوند؟
  • آیا هیچ کلید واقعی API در کد وجود ندارد؟
  • آیا خطاهای شبکه و پاسخ ناقص تست شده‌اند؟
  • آیا رفتار کاربر به‌جای جزئیات داخلی تست شده است؟
  • آیا تست‌ها در CI اجرا می‌شوند؟
  • آیا نسخه Node در سیستم توسعه و CI یکسان است؟
  • آیا Build پس از تست موفق اجرا می‌شود؟
  • آیا گزارش Coverage فایل‌های مهم را شامل می‌شود؟
  • آیا Snapshotها بررسی شده‌اند؟
  • آیا تست‌های Skip شده دلیل مشخص دارند؟
  • آیا عبارت .only پیش از Commit حذف شده است؟

پرسش‌های متداول درباره Vitest

آیا Vitest فقط برای پروژه‌های Vite است؟

Vitest با Vite هماهنگی عمیقی دارد، اما می‌توان آن را در بسیاری از پروژه‌های JavaScript و TypeScript نیز استفاده کرد. با این حال، در پروژه‌های مبتنی بر Vite بیشترین سادگی و هماهنگی را ارائه می‌دهد.

آیا Vitest جایگزین Jest است؟

برای بسیاری از پروژه‌های مدرن، به‌خصوص پروژه‌های Vite و ESM، Vitest می‌تواند جایگزین مناسبی برای Jest باشد. اما مهاجرت باید با توجه به Pluginها، Mockها، Snapshotها و تنظیمات موجود انجام شود.

آیا Vitest برای React مناسب است؟

بله. ترکیب Vitest، jsdom و React Testing Library یکی از روش‌های متداول تست پروژه‌های React و TypeScript است.

آیا می‌توان API را با Vitest تست کرد؟

بله. می‌توانید توابع Client، Handlerهای Backend، اعتبارسنجی پاسخ و مدیریت خطا را تست کنید. برای Unit Test بهتر است درخواست‌های خارجی Mock شوند.

آیا اجرای Mock به API واقعی درخواست می‌فرستد؟

خیر. اگر fetch یا لایه HTTP به‌درستی Mock شده باشد، درخواست واقعی ارسال نمی‌شود.

آیا Coverage صددرصد ضروری است؟

خیر. پوشش صددرصد همیشه اقتصادی یا مفید نیست. تمرکز باید روی منطق پرریسک، مسیرهای اصلی و سناریوهای شکست باشد.

Vitest بهتر است یا Playwright؟

کاربرد آن‌ها متفاوت است. Vitest برای Unit و Integration Test مناسب است، در حالی که Playwright بیشتر برای تست End-to-End در مرورگر واقعی استفاده می‌شود.

آیا می‌توان Vitest را در GitHub Actions اجرا کرد؟

بله. دستور vitest run برای محیط CI مناسب است و می‌توان آن را همراه Type Check، Lint، Coverage و Build اجرا کرد.

جمع‌بندی

Vitest یک ابزار سریع و مدرن برای تست برنامه‌های JavaScript، TypeScript و React است. هماهنگی مستقیم با Vite، پشتیبانی از ESM و TypeScript، API آشنا، قابلیت Mock، Snapshot، Coverage و اجرای تست کامپوننت‌ها باعث شده است راه‌اندازی آن در پروژه‌های جدید ساده باشد.

برای شروع لازم نیست تمام پروژه را یک‌باره تست کنید. ابتدا سراغ بخش‌های پرریسک و مهم بروید:

  1. توابع خالص و محاسباتی را تست کنید.
  2. اعتبارسنجی ورودی و مدیریت خطا را پوشش دهید.
  3. درخواست‌های خارجی را Mock کنید.
  4. رفتار اصلی کامپوننت‌های React را آزمایش کنید.
  5. تست‌ها را در CI اجرا کنید.
  6. به‌تدریج Coverage بخش‌های حیاتی را افزایش دهید.

اگر برنامه شما به مدل‌های هوش مصنوعی متصل می‌شود، Mock کردن پاسخ API باعث می‌شود تست‌ها سریع، قابل‌تکرار و بدون مصرف واقعی سرویس اجرا شوند. برای ساخت سرویس‌های مبتنی بر API هوش مصنوعی می‌توانید در درواره ثبت‌نام کنید و مدل مناسب پروژه خود را از فهرست مدل‌های درواره انتخاب کنید.

مقالات مرتبط

منابع تکمیلی

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.