Vue.js: الاختبارات الوحدوية و E2E
آخر تحديث: 2026-08-26
يُعد الاختبار الفردي حجر الزاوية في ضمان جودة الكود — فهو يتيح إعادة هيكلة المكونات دون خوف من حدوث أعطال، ويمكّن نظام التكامل المستمر (CI) من تشغيل الاختبارات تلقائيًا، ويمنح أعضاء الفريق الجدد الثقة اللازمة لإجراء تغييرات على الكود. يوصي Vue 3 باستخدام Vitest (framework اختبار من الجيل التالي أسرع بعشر مرات من Jest) + Vue Test Utils (أداة اختبار المكونات الرسمية).
يحاكي الاختبار من طرف إلى طرف (E2E) تفاعلات المستخدمين الحقيقية — ويُعد «Playwright» حاليًا الأداة الأكثر شيوعًا في مجال الاختبار من طرف إلى طرف (وهو أسرع من «Cypress» ويعمل عبر المتصفحات المختلفة). تتناول هذه الدورة framework شامل للاختبار يشمل كلاً من الاختبار الوحدوي والاختبار من طرف إلى طرف.
1. ما ستتعلمه
- تكوين Vitest وواجهات برمجة التطبيقات الأربعة الأساسية
- تثبيت المكونات في Vue Test Utils (@vue/test-utils)
- 5 سيناريوهات رئيسية لاختبار المكونات (props / emit / slot / evento / async)
- 5 تقنيات أساسية للتعبير عن الرأي
- الاختبار الشامل لبرنامج Playwright
- تقرير التغطية
- 5 أخطاء شائعة
2. التحديات التي يواجهها الفريق الذي يعاني من «رهاب إعادة الهيكلة»
(1) المشكلة: إذا تم تعديل أحد المكونات بشكل خاطئ، فإن الموقع بأكمله يتعطل
كان لدى فريق أليس نظام إدارة للتجارة الإلكترونية يضم أكثر من 100 مكون:
// ❌ 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
// 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])
})
})
قم بإجراء الاختبار:
$ npx vitest run
✓ ProductCard > renders product name and price
✓ ProductCard > emits add-to-cart event
2 passed
يُنفَّذ في غضون 5 ثوانٍ، دون أي إنذارات كاذبة. ستعرف فور إجراء تغيير في الكود ما إذا كان هناك خلل فيه أم لا.
(3) الإيرادات
بعد إضافة الاختبارات:
- استعادة الثقة: تحسن بنسبة 100٪ (دون خوف من تفاقم الوضع)
- الكشف المبكر عن الأخطاء: يتم اكتشاف 80% منها خلال مرحلة التطوير
- الغرض من الوثائق: يُعد الاختبار بمثابة دليل المستخدم الخاص بالمكون.
- أتمتة CI: تشغيل الاختبارات تلقائيًا على طلبات السحب
3. تهيئة Vitest
(1) التثبيت
npm install -D vitest @vue/test-utils @vitest/coverage-v8 jsdom
(2) التكوين الأساسي
// 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 واجهات برمجة تطبيقات أساسية رئيسية
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 واجهات برمجة تطبيقات رئيسية
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 سيناريوهات اختبار رئيسية
// 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 تقنيات أساسية للتعبير عن الرأي
// 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) التثبيت
npm install -D @playwright/test
npx playwright install
(2) التهيئة
// 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)
// 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
# 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. اختبار المكونات الخمسة الرئيسية
// 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 تقنيات أساسية للتأكيد
// 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
// 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 (بطيء) |
| مكتبة الاختبارات | ⭐⭐⭐⭐ | اختبار المكونات |
❓ أسئلة شائعة
mount أم shallowMount؟shallowMount لاختبارات الوحدات (أسرع، وأكثر عزلًا). استخدم mount لاختبارات التكامل (أكثر واقعية). التوصية: استخدم shallowMount لاختبارات المكونات، وmount لبعض المكونات الرئيسية.npm test في GitHub Actions أو GitLab CI. وقم بحظر دمج طلبات السحب (PR) في status فشل الاختبارات.📖 ملخص
- Vitest هو framework الاختبار الموصى به لـ Vue 3 (أسرع بـ 10 أضعاف من Jest)
- أدوات اختبار Vue: mount / shallowMount / 5 واجهات برمجة تطبيقات رئيسية
- 5 أنواع من اختبارات المكونات: props / emit / slot / async / provide
- 5 أنواع من التأكيدات: النص / الفئة / العنصر / الحدث / غير المتزامن
- تُعد «Playwright» الأداة الموصى بها لتنفيذ المعالجة من البداية إلى النهاية (سريعة / متوافقة مع جميع المتصفحات)
- تغطية الاختبار: 80%+ في المواد الأساسية، 60%+ بشكل عام
- 70% اختبار الوحدات + 20% اختبار التكامل + 10% اختبار من البداية إلى النهاية
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
اكتب 3 اختبارات لـ ProductCard:
- عرض العناصر المسرحية
- إصدار حدث
- الstatus الحدية (المخزون يساوي 0)
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
اكتب اختبارات شاملة لعربة التسوق
store:- 5 إجراءات (addItem / removeItem / clear / load / save)
- 3 دالات استرجاع (itemCount / totalPrice / isEmpty)
- إعداد اختبار Pinia
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تنفيذ «framework شامل للاختبار»:
- 10 اختبارات وحدة للمكونات (3–5 اختبارات لكل مكون)
- 5 اختبارات قابلة للتركيب
- 5 اختبارات شاملة (تسجيل الدخول / التسجيل / البحث / سلة التسوق / إتمام عملية الشراء)
- حد التغطية: 80%
- تكامل CI (GitHub Actions)
- اختبار التوافق بين المتصفحات باستخدام Vitest وPlaywright