Vue.js: الاختبارات الوحدوية و E2E

آخر تحديث: 2026-08-26

يُعد الاختبار الفردي حجر الزاوية في ضمان جودة الكود — فهو يتيح إعادة هيكلة المكونات دون خوف من حدوث أعطال، ويمكّن نظام التكامل المستمر (CI) من تشغيل الاختبارات تلقائيًا، ويمنح أعضاء الفريق الجدد الثقة اللازمة لإجراء تغييرات على الكود. يوصي Vue 3 باستخدام Vitest (framework اختبار من الجيل التالي أسرع بعشر مرات من Jest) + Vue Test Utils (أداة اختبار المكونات الرسمية).

يحاكي الاختبار من طرف إلى طرف (E2E) تفاعلات المستخدمين الحقيقية — ويُعد «Playwright» حاليًا الأداة الأكثر شيوعًا في مجال الاختبار من طرف إلى طرف (وهو أسرع من «Cypress» ويعمل عبر المتصفحات المختلفة). تتناول هذه الدورة framework شامل للاختبار يشمل كلاً من الاختبار الوحدوي والاختبار من طرف إلى طرف.

1. ما ستتعلمه



2. التحديات التي يواجهها الفريق الذي يعاني من «رهاب إعادة الهيكلة»

(1) المشكلة: إذا تم تعديل أحد المكونات بشكل خاطئ، فإن الموقع بأكمله يتعطل

كان لدى فريق أليس نظام إدارة للتجارة الإلكترونية يضم أكثر من 100 مكون:

JS
// ❌ The "Broken" Version:Not tested,Alice I don't dare to refactor
// ProductCard.vue Changed 1 line, The entire home page is a blank screen
// No one noticed, Day 2 not until a user filed a complaint they discovered
// Team:"Don't move this component,We don't know what it will destroy."

مدير المنتج تشارلي:

"أليس، نحن بحاجة إلى اختبارات. نريد أن نتمكن من إعادة هيكلة الكود دون خوف. نحتاج إلى 1) اختبارات وحدة للمكونات، 2) اختبارات من طرف إلى طرف (E2E) للتدفقات الحرجة."

(2) حل أدوات الاختبار Vitest + Vue

JS
// tests/unit/ProductCard.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductCard from '@/components/ProductCard.vue'

describe('ProductCard', () => {
  it('renders product name and price', () => {
    const product = { id: 1, name: 'iPhone', price: 999 }
    const wrapper = mount(ProductCard, { props: { product } })
    
    expect(wrapper.text()).toContain('iPhone')
    expect(wrapper.text()).toContain('$999')
  })
  
  it('emits add-to-cart evento', async () => {
    const wrapper = mount(ProductCard, { 
      props: { product: { id: 1, name: 'iPhone', price: 999 } } 
    })
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('add-to-cart')).toBeTruthy()
    expect(wrapper.emitted('add-to-cart')[0]).toEqual([1])
  })
})

قم بإجراء الاختبار:

BASH
$ npx vitest run
✓ ProductCard > renders product name and price
✓ ProductCard > emits add-to-cart event
2 passed

يُنفَّذ في غضون 5 ثوانٍ، دون أي إنذارات كاذبة. ستعرف فور إجراء تغيير في الكود ما إذا كان هناك خلل فيه أم لا.

(3) الإيرادات

بعد إضافة الاختبارات:



3. تهيئة Vitest

(1) التثبيت

BASH
npm install -D vitest @vue/test-utils @vitest/coverage-v8 jsdom

(2) التكوين الأساسي

TS
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  
  test: {
    // Test Environment
    environment: 'jsdom',  // or 'happy-dom' / 'node'
    
    // Matching Files
    include: ['tests/**/*.test.ts', 'src/**/*.test.ts'],
    
    // Coverage Rate
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json'],
      exclude: ['node_modules/', 'tests/']
    },
    
    // Global setup
    setupFiles: ['./tests/setup.ts']
  },
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
})

(3) 4 واجهات برمجة تطبيقات أساسية رئيسية

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

// 1. describe:Test Suite
describe('Calculator', () => { })

// 2. it:Single Test
it('adds 1 + 1 to equal 2', () => { })

// 3. expect:Assertion
expect(1 + 1).toBe(2)
expect(arr).toContain('apple')
expect(obj).toMatchObject({ name: 'Alice' })

// 4. vi:mock / spy
vi.fn()                  // mock Function
vi.mock('./api')         // mock Module
vi.spyOn(obj, 'method')  // spy Methods

(4) Vitest مقابل Jest

البعد هو Vitest
وقت بدء التشغيل 3–5 ثوانٍ < 1 ثانية
وضع الاستماع 2–3 ثوانٍ < 100 مللي ثانية
دعم ESM يتطلب التهيئة أصلي
TypeScript يتطلب ts-jest أصلي
التوافق شامل واجهة برمجة تطبيقات متوافقة مع Jest
التقييم قديم ⭐⭐⭐⭐⭐


4. اختبار مكونات Vue Test Utils

(1) 5 واجهات برمجة تطبيقات رئيسية

JS
import { mount, shallowMount, RouterLinkStub } from '@vue/test-utils'

// 1. mount:Full Mount(Child components are also mounted)
const wrapper = mount(Component, { props: { ... } })

// 2. shallowMount: Shallow Mount (Child components are stubbed)
const wrapper = shallowMount(Component, { props: { ... } })

// 3. Find an Element
wrapper.find('button')              // First match
wrapper.findAll('li')              // All matches
wrapper.findComponent(MyComponent)  // Find Child Components
wrapper.get('#submit')             // Must be found(Error not found)

// 4. Trigger Event
await wrapper.find('button').trigger('click')
await wrapper.find('input').setValue('hello')

// 5. Assertion
expect(wrapper.text()).toContain('Hello')
expect(wrapper.find('h1').text()).toBe('Title')
expect(wrapper.emitted('add-to-cart')).toBeTruthy()

(2) 5 سيناريوهات اختبار رئيسية

JS
// 1. props Test
it('renders props correctly', () => {
  const wrapper = mount(MyComponent, { props: { title: 'Hello' } })
  expect(wrapper.text()).toContain('Hello')
})

// 2. emit Test
it('emits event on click', async () => {
  const wrapper = mount(MyComponent)
  await wrapper.find('button').trigger('click')
  expect(wrapper.emitted('submit')).toBeTruthy()
})

// 3. slot Test
it('renders default slot', () => {
  const wrapper = mount(MyComponent, {
    slots: { default: '<p>Slot content</p>' }
  })
  expect(wrapper.html()).toContain('Slot content')
})

// 4. Asynchronous Testing
it('loads data on mount', async () => {
  const wrapper = mount(MyComponent)
  await flushPromises()  // Wait for asynchronous completion
  expect(wrapper.text()).toContain('Loaded')
})

// 5. provide/inject Test
it('uses provided value', () => {
  const wrapper = mount(MyComponent, {
    provide: { theme: 'dark' }
  })
  expect(wrapper.find('.dark').exists()).toBe(true)
})

(3) 5 تقنيات أساسية للتعبير عن الرأي

JS
// 1. The text contains
expect(wrapper.text()).toContain('Hello')

// 2. class Existence
expect(wrapper.find('.active').exists()).toBe(true)

// 3. Elemental Presence
expect(wrapper.find('button').exists()).toBe(true)

// 4. Event Trigger
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')[0]).toEqual([arg1, arg2])

// 5. Asynchronous operation completed
await wrapper.vm.$nextTick()
await flushPromises()


5. الاختبار الشامل لبرنامج Playwright

(1) التثبيت

BASH
npm install -D @playwright/test
npx playwright install

(2) التهيئة

TS
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry'
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } }
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI
  }
})

(3) 5 سيناريوهات رئيسية لاختبار «من طرف إلى طرف» (E2E)

TS
// 1. Login Process
import { test, expect } from '@playwright/test'

test('user can login', async ({ page }) => {
  await page.goto('/login')
  await page.fill('input[name="username"]', 'alice@example.com')
  await page.fill('input[name="password"]', 'password123')
  await page.click('button[type="submit"]')
  
  await expect(page).toHaveURL('/dashboard')
  await expect(page.locator('h1')).toContainText('Welcome, Alice')
})

// 2. Add to Cart
test('add product to cart', async ({ page }) => {
  await page.goto('/products/1')
  await page.click('button:has-text("Add to Cart")')
  
  await expect(page.locator('.cart-count')).toHaveText('1')
})

// 3. Form Submission
test('submit form with validation', async ({ page }) => {
  await page.goto('/contact')
  await page.fill('input[name="email"]', 'invalid-email')
  await page.click('button[type="submit"]')
  
  await expect(page.locator('.error')).toBeVisible()
})

// 4. Route Redirection
test('navigate to products page', async ({ page }) => {
  await page.goto('/')
  await page.click('a:has-text("Products")')
  
  await expect(page).toHaveURL('/products')
})

// 5. Asynchronous Loading
test('loads data after navigation', async ({ page }) => {
  await page.goto('/dashboard')
  
  // Waiting for data to load
  await page.waitForSelector('.chart-loaded')
  await expect(page.locator('.chart-loaded')).toBeVisible()
})

(4) تشغيل اختبارات E2E

BASH
# Run all tests
npx playwright test

# Run a Specific File
npx playwright test tests/e2e/login.spec.ts

# Debug Mode(Open your browser)
npx playwright test --debug

# UI Pattern(Visualization)
npx playwright test --ui


6. مثال كامل: 5 سيناريوهات اختبار رئيسية

▶ مثال: 1. اختبار المكونات الخمسة الرئيسية

JS
// 1. props
it('renders props', () => { ... })

// 2. emit
it('emits event', async () => { ... })

// 3. slot
it('renders slot', () => { ... })

// 4. async
it('loads data', async () => { ... })

// 5. provide
it('uses provide', () => { ... })
▶ جرّب الكود

▶ مثال: 2. 5 تقنيات أساسية للتأكيد

JS
// 1. Text
expect(wrapper.text()).toContain('Hello')

// 2. class
expect(wrapper.find('.active').exists()).toBe(true)

// 3. Element
expect(wrapper.find('button').exists()).toBe(true)

// 4. Event
expect(wrapper.emitted('click')).toBeTruthy()

// 5. Asynchronous
await wrapper.vm.$nextTick()
▶ جرّب الكود

▶ مثال: 3. 5 سيناريوهات رئيسية لـ E2E

TS
// 1. Log In
test('login', async ({ page }) => { ... })

// 2. Shopping Cart
test('add to cart', async ({ page }) => { ... })

// 3. Form
test('form submit', async ({ page }) => { ... })

// 4. Routing
test('navigate', async ({ page }) => { ... })

// 5. Asynchronous
test('async load', async ({ page }) => { ... })
▶ جرّب الكود

▶ مثال: 4. مرجع سريع لـ 5 أخطاء شائعة

الخطأ الأعراض الحل
لا يمكن للاختبار العثور على المكون خطأ في الاستيراد تحقق من أسماء المسارات المستعارة
الاختبار غير المتزامن دون انتظار يفشل دائمًا استخدم await + flushPromises
حدث التشغيل لا يعمل لم يتم تشغيل المستمع استخدام "await" بعد "trigger"
انتهاء مهلة E2E لم يتم العثور على العنصر waitForSelector
تغطية منخفضة لم تُجرَ أي اختبارات الحد الأدنى الإلزامي للتغطية

▶ مثال: 5. مقارنة بين 5 أدوات اختبار رئيسية

الأداة السرعة الاستخدام
Vitest ⭐⭐⭐⭐⭐ اختبار الوحدات
أدوات اختبار Vue ⭐⭐⭐⭐⭐ اختبار المكونات
كاتب مسرحي ⭐⭐⭐⭐ اختبار من البداية إلى النهاية
Cypress ⭐⭐⭐ E2E (بطيء)
مكتبة الاختبارات ⭐⭐⭐⭐ اختبار المكونات


❓ أسئلة شائعة

س أيهما يجب أن أختار، Vitest أم Jest؟
ج Vitest. استخدم Vitest لجميع المشاريع الجديدة (أسرع بـ 10 أضعاف، ودعم أصلي لـ ESM/TS). استخدم Jest لصيانة المشاريع القديمة.
س أيهما يجب أن أختار، mount أم shallowMount؟
ج استخدم shallowMount لاختبارات الوحدات (أسرع، وأكثر عزلًا). استخدم mount لاختبارات التكامل (أكثر واقعية). التوصية: استخدم shallowMount لاختبارات المكونات، وmount لبعض المكونات الرئيسية.
س ما هو معدل تغطية الاختبار المناسب؟
ج 80%+ للمكونات الأساسية، و90%+ للأدوات المساعدة، و60%+ بشكل عام. لا داعي للسعي إلى تحقيق 100% (فهذا غير عملي).
س Playwright أم Cypress — أيهما يجب أن أختار؟
ج Playwright. فهو أسرع بنحو الضعف، ويدعم متصفحات متعددة (Chromium، Firefox، WebKit)، ويتميز بواجهة برمجة تطبيقات (API) أفضل. أما Cypress فيدعم فقط المتصفحات القائمة على Chromium.
س ما هي نسبة اختبارات الوحدة إلى اختبارات E2E؟
ج 70% اختبارات وحدة + 20% اختبارات تكامل + 10% اختبارات E2E. اختبارات E2E بطيئة وهشة، في حين أن اختبارات الوحدة توفر أفضل عائد مقابل التكلفة.
س هل يجب محاكاة جميع التبعيات أثناء الاختبار؟
ج بالنسبة لاختبارات الوحدة، قم بمحاكاة واجهات برمجة التطبيقات الخارجية (fetch، localStorage). أما بالنسبة لاختبارات التكامل، فاستخدم التبعيات الحقيقية. وبالنسبة لاختبارات E2E، استخدم كل شيء كما هو.
س كيف يمكنني إجراء الاختبارات باستخدام CI؟
ج أضف الخطوة npm test في GitHub Actions أو GitLab CI. وقم بحظر دمج طلبات السحب (PR) في status فشل الاختبارات.

📖 ملخص


📝 تمارين

  1. أسئلة أساسية (مستوى الصعوبة: ⭐)

    اكتب 3 اختبارات لـ ProductCard:

    • عرض العناصر المسرحية
    • إصدار حدث
    • الstatus الحدية (المخزون يساوي 0)
  2. مسائل متقدمة (مستوى الصعوبة: ⭐⭐)

    اكتب اختبارات شاملة لعربة التسوق store:

    • 5 إجراءات (addItem / removeItem / clear / load / save)
    • 3 دالات استرجاع (itemCount / totalPrice / isEmpty)
    • إعداد اختبار Pinia
  3. مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)

    تنفيذ «framework شامل للاختبار»:

    1. 10 اختبارات وحدة للمكونات (3–5 اختبارات لكل مكون)
    2. 5 اختبارات قابلة للتركيب
    3. 5 اختبارات شاملة (تسجيل الدخول / التسجيل / البحث / سلة التسوق / إتمام عملية الشراء)
    4. حد التغطية: 80%
    5. تكامل CI (GitHub Actions)
    6. اختبار التوافق بين المتصفحات باستخدام Vitest وPlaywright
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%