404 Not Found

404 Not Found


nginx

اختبار Vitest + Playwright

أصبح MegaShop متاحاً، لكن كل إصدار كان به أخطاء—أخطاء في حساب سلة التسوق، أعطال عرضية في تسجيل الدخول، وأخطاء 404 على صفحات المنتجات. قرر Charlie إنشاء نظام اختبار آلي، باستخدام Vitest لاختبارات الوحدة وPlaywright لاختبارات النهاية إلى النهاية (E2E)، لضمان جودة موثوقة مع كل إصدار.

1. ما ستتعلمه


2. قصة حقيقية لمهندس معماري

(1) نقطة الألم: الاختبار اليدوي لا يغطي جميع السيناريوهات

قبل كل إصدار، يختبر Bob يدوياً MegaShop—الصفحة الرئيسية، صفحات المنتجات، سلة التسوق، الدفع، ولوحة الإدارة. 5 متصفحات x 3 لغات x 20 صفحة = 300 اختبار يدوي. مع فجوة تغطية اختبار 30٪، يتم اكتشاف خطأين على الأقل في الإنتاج مع كل إصدار.

(2) حلول الاختبار الآلي

Vitest + Playwright: أتمتة تشغيل الاختبارات:

TYPESCRIPT
// Vitest: اختبار وحدة لـ usePriceFormat
test('formats USD price correctly', () => {
  const price = ref(2999.99)
  const { formatted } = usePriceFormat(price)
  expect(formatted.value).toBe('$2,999.99')
})

(3) النتائج: 0 اختبار يدوي + 0 خطأ في الإنتاج

الاختبار الآلي يغطي العمليات الأساسية؛ CI يعمل تلقائياً مع كل التزام، مما يقلل معدل إغفال الاختبار إلى 2٪ ويصل بعدد أخطاء الإنتاج إلى 0.


3. هرم الاختبار

(1) التسلسل الهرمي للاختبار

100%
graph TB
    A[اختبارات E2E - Playwright<br/>مسارات المستخدم الحرجة] --> B[اختبارات التكامل<br/>تفاعلات API + المكوّنات]
    B --> C[اختبارات الوحدة - Vitest<br/>Composables / Stores / الأدوات]
    
    style A fill:#f96,stroke:#333
    style B fill:#ff9,stroke:#333
    style C fill:#9f9,stroke:#333

(2) مقارنة أنواع الاختبار

البُعد اختبار الوحدة اختبار التكامل اختبار E2E
الأدوات Vitest Vitest Playwright
النطاق دالة/مكوّن واحد تفاعل بين وحدات متعددة سير مستخدم كامل
السرعة ⚡⚡⚡ سريع ⚡⚡ متوسط 🐢 بطيء
الكمية عالية متوسطة منخفضة
القيمة صحة المنطق اتساق الواجهة ضمان سير المستخدم

4. تكوين Vitest واختبارات الوحدة

(1) ▶ مثال: التثبيت والتكوين

BASH
npm install -D vitest @nuxt/test-utils @vue/test-utils happy-dom

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح
TYPESCRIPT
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      exclude: ['node_modules', '.nuxt']
    }
  }
})

(2) ▶ مثال: اختبارات وحدة Composable

TYPESCRIPT
// tests/composables/usePriceFormat.test.ts
import { ref } from 'vue'

describe('usePriceFormat', () => {
  test('formats USD price with comma separator', () => {
    const price = ref(2999.99)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$2,999.99')
  })

  test('formats JPY price with no decimals', () => {
    const price = ref(29999)
    const { formatted } = usePriceFormat(price, { currency: 'JPY', locale: 'ja-JP' })
    expect(formatted.value).toBe('¥29,999')
  })

  test('reactively updates when price changes', () => {
    const price = ref(100)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$100.00')
    price.value = 200
    expect(formatted.value).toBe('$200.00')
  })

  test('handles zero price', () => {
    const price = ref(0)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$0.00')
  })

  test('handles raw number input (not ref)', () => {
    const { formatted } = usePriceFormat(50.5)
    expect(formatted.value).toBe('$50.50')
  })
})

الناتج:

TEXT
// التنفيذ ناجح

(3) ▶ مثال: اختبار Pinia Store

TYPESCRIPT
// tests/stores/cart.test.ts
import { setActivePinia, createPinia } from 'pinia'

describe('useCartStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  test('adds item to cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' })
    expect(store.items.length).toBe(1)
    expect(store.totalItems).toBe(1)
  })

  test('increments quantity for existing item', () => {
    const store = useCartStore()
    const product = { id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' }
    store.addItem(product)
    store.addItem(product)
    expect(store.items.length).toBe(1)
    expect(store.items[0].quantity).toBe(2)
  })

  test('removes item from cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' })
    store.removeItem(1)
    expect(store.items.length).toBe(0)
  })

  test('calculates total price correctly', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' })
    store.addItem({ id: 2, name: 'B', price: 200, image: '/b.jpg' })
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' }) // الكمية = 2
    expect(store.totalPrice).toBe(400) // 100*2 + 200
  })

  test('clears cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' })
    store.clearCart()
    expect(store.items.length).toBe(0)
    expect(store.totalPrice).toBe(0)
  })
})

الناتج:

TEXT
// التنفيذ ناجح

5. اختبار المكوّنات

(1) ▶ مثال: اختبار مكوّن mountSuspended

TYPESCRIPT
// tests/components/ProductCard.test.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'

describe('ProductCard', () => {
  const mockProduct = {
    id: 1, name: 'Premium Headphones', price: 299.99,
    image: '/headphones.jpg', inStock: true
  }

  test('renders product name and price', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: mockProduct }
    })
    expect(wrapper.text()).toContain('Premium Headphones')
    expect(wrapper.text()).toContain('299.99')
  })

  test('emits add-to-cart event on button click', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: mockProduct }
    })
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('add-to-cart')).toBeTruthy()
    expect(wrapper.emitted('add-to-cart')[0][0]).toEqual(mockProduct)
  })

  test('shows out of stock when not available', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: { ...mockProduct, inStock: false } }
    })
    expect(wrapper.text()).toContain('Out of Stock')
  })
})

الناتج:

TEXT
// التنفيذ ناجح

6. اختبار E2E عبر Playwright

(1) ▶ مثال: تكوين Playwright

TYPESCRIPT
// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  baseURL: 'http://localhost:3000',
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI
  },
  use: {
    headless: true,
    screenshot: 'only-on-failure'
  }
})

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: سير عمل سلة التسوق من النهاية إلى النهاية

TYPESCRIPT
// e2e/cart-flow.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Shopping Cart Flow', () => {
  test('add product to cart and verify', async ({ page }) => {
    // الانتقال إلى قائمة المنتجات
    await page.goto('/products')

    // انتظار تحميل المنتجات
    await page.waitForSelector('.product-card')

    // النقر على زر "أضف إلى السلة" للمنتج الأول
    await page.locator('.product-card:first-child button').click()

    // التحقق من تحديث عدد السلة
    const cartCount = page.locator('.cart-count')
    await expect(cartCount).toHaveText('1')

    // الانتقال إلى صفحة السلة
    await page.goto('/cart')

    // التحقق من وجود عنصر في السلة
    await expect(page.locator('.cart-item')).toHaveCount(1)
  })

  test('complete checkout flow', async ({ page }) => {
    // تسجيل الدخول أولاً
    await page.goto('/login')
    await page.fill('input[type="email"]', 'alice@example.com')
    await page.fill('input[type="password"]', 'password123')
    await page.click('button[type="submit"]')

    // إضافة عنصر والانتقال إلى الدفع
    await page.goto('/products/1')
    await page.click('button:has-text("Add to Cart")')
    await page.click('a:has-text("Cart")')
    await page.click('a:has-text("Proceed to Checkout")')

    // التحقق من صفحة الدفع
    await expect(page.locator('h1')).toHaveText(/checkout/i)
  })
})

الناتج:

TEXT
// التنفيذ ناجح

7. اختبار API

(1) ▶ مثال: اختبار API الخادم

TYPESCRIPT
// tests/api/products.test.ts
import { setupTest } from '@nuxt/test-utils'

describe('Products API', () => {
  setupTest()

  test('GET /api/products returns product list', async () => {
    const response = await $fetch('/api/products')
    expect(response.items).toBeDefined()
    expect(response.total).toBeGreaterThan(0)
    expect(response.page).toBe(1)
  })

  test('GET /api/products/:id returns product detail', async () => {
    const product = await $fetch('/api/products/1')
    expect(product.id).toBe(1)
    expect(product.name).toBeDefined()
    expect(product.price).toBeDefined()
  })

  test('GET /api/products/:id returns 404 for invalid id', async () => {
    await expect($fetch('/api/products/99999')).rejects.toThrow('404')
  })

  test('GET /api/products supports pagination', async () => {
    const page1 = await $fetch('/api/products?page=1&limit=5')
    const page2 = await $fetch('/api/products?page=2&limit=5')
    expect(page1.items.length).toBeLessThanOrEqual(5)
    expect(page2.items[0].id).not.toBe(page1.items[0].id)
  })

  test('GET /api/products supports category filter', async () => {
    const result = await $fetch('/api/products?category=electronics')
    result.items.forEach((item: any) => {
      expect(item.category).toBe('electronics')
    })
  })
})

الناتج:

TEXT
// التنفيذ ناجح

8. مثال شامل: إطار اختبار MegaShop

JSON
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:all": "npm run test && npm run test:e2e"
  }
}
نوع الاختبار الأمر التغطية متى يُشغّل
اختبارات الوحدة vitest Composable/Store/أدوات كل التزام
اختبار المكوّنات vitest مكوّنات Vue كل التزام
اختبار API vitest API الخادم كل إرسال
اختبار E2E Playwright سير المستخدم قبل دمج PR
التغطية vitest --coverage الكل خط CI

❓ أسئلة شائعة

س ما الفرق بين Vitest وJest؟
ج Vitest مبني على Vite ويشارك تكوينات البناء مع Nuxt 3، مما ينتج أوقات بدء أسرع. Jest يتطلب تكوين Babel/webpack إضافياً. Nuxt 3 يوصي باستخدام Vitest.
س ما الفرق بين mountSuspended وmount؟
ج mount هي دالة التركيب الأساسية التي توفرها Vue Test Utils، بينما mountSuspended توفرها @nuxt/test-utils وتدعم السياق الكامل، بما في ذلك إضافات Nuxt والاستيرادات التلقائية وComposables.
س ماذا أفعل إذا كان اختبار E2E بطيئاً جداً؟
ج اختبر فقط مسارات المستخدم الرئيسية (سلة التسوق/الدفع/تسجيل الدخول)، وليس كل صفحة. اختبار المكوّنات يغطي منطق واجهة المستخدم، بينما اختبار E2E يغطي التدفقات من النهاية إلى النهاية.
س كيف أختبر سلوك SSR؟
ج استخدم طريقة $fetch من @nuxt/test-utils لطلب HTML المعروض من جانب الخادم مباشرة وتحقق من أن HTML يحتوي على البيانات. يمكنك أيضاً استخدام Playwright لتعطيل JavaScript وفحص الصفحة.
س هل يتطلب اختبار Pinia Store وهم API؟
ج إجراء $fetch المستدعى داخل Store يحتاج إلى وهم. استخدم vitest.mock لاعتراض $fetch، أو استخرج استدعاء API إلى Composable لتسهيل الوهم.
س كيف يعمل Vitest وPlaywright معاً في CI؟
ج Vitest يعمل بعد الفحص (سريع، 1-2 دقيقة)، وPlaywright يعمل بعد البناء (بطيء، 5-10 دقائق). كلاهما يعمل عند إنشاء طلب سحب؛ فقط Vitest يعمل عند الدفع إلى الفرع الرئيسي.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): ثبّت Vitest و@nuxt/test-utils، واكتب 5 اختبارات وحدة لـ usePriceFormat.
  2. تمرين متقدم (الصعوبة: ⭐⭐): اكتب اختبار CRUD كامل لـ cartStore واختبار مكوّن لـ ProductCard.
  3. تحدٍ (الصعوبة: ⭐⭐⭐): اكتب اختبار E2E كامل لسلة تسوق باستخدام Playwright: تصفح المنتجات → إضافة إلى السلة → عرض السلة → الدفع

---|

Web-Tutorial.com

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

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

100%