الاختبارات و TDD

الاختبار الشامل باستخدام Playwright

35 دقيقة الدرس 14 من 35

مقدمة إلى Playwright

Playwright هو إطار عمل اختبار شامل حديث تم تطويره بواسطة Microsoft يمكّن من الاختبار الموثوق عبر جميع محركات المتصفح الرئيسية: Chromium و Firefox و WebKit. يوفر واجهة برمجة تطبيقات موحدة لأتمتة المتصفح مع تجربة مطور ممتازة وميزات قوية.

لماذا Playwright؟

  • الاختبار عبر المتصفحات: اختبر على Chromium و Firefox و WebKit بواجهة برمجة تطبيقات واحدة
  • محاكاة الهاتف المحمول: اختبر التصميمات المتجاوبة مع محاكاة الجهاز
  • الانتظار التلقائي: انتظار ذكي للعناصر لتكون جاهزة
  • التنفيذ المتوازي: تشغيل الاختبارات بالتوازي عبر متصفحات متعددة
  • اعتراض الشبكة: محاكاة وتعديل طلبات الشبكة
  • التتبع والتصحيح: تسجيل تنفيذ الاختبار مع لقطات الشاشة ومقاطع الفيديو
  • دعم TypeScript: دعم TypeScript من الدرجة الأولى خارج الصندوق
ملاحظة: بينما يركز Cypress على تجربة المطور، يتفوق Playwright في الاختبار عبر المتصفحات وسيناريوهات الأتمتة المتقدمة.

تثبيت Playwright

تثبيت Playwright وتهيئة التكوين:

# تثبيت Playwright npm init playwright@latest # أو التثبيت يدوياً npm install --save-dev @playwright/test # تثبيت المتصفحات npx playwright install

ينشئ التثبيت هذا الهيكل:

tests/ # ملفات الاختبار playwright.config.ts # التكوين .github/workflows/ # تكوين CI/CD

التكوين

قم بتكوين Playwright في playwright.config.ts:

import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // دليل الاختبار testDir: './tests', // تشغيل الاختبارات بالتوازي fullyParallel: true, // فشل البناء على CI إذا تركت test.only عن طريق الخطأ forbidOnly: !!process.env.CI, // إعادة محاولة الاختبارات الفاشلة retries: process.env.CI ? 2 : 0, // عدد العمال (التنفيذ المتوازي) workers: process.env.CI ? 1 : undefined, // المراسل reporter: 'html', // الإعدادات المشتركة لجميع الاختبارات use: { // عنوان URL الأساسي baseURL: 'http://localhost:3000', // جمع الأثر عند إعادة المحاولة الأولى trace: 'on-first-retry', // لقطة الشاشة عند الفشل screenshot: 'only-on-failure', }, // تكوين المشاريع للمتصفحات الرئيسية projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, // منافذ عرض الهاتف المحمول { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, { name: 'Mobile Safari', use: { ...devices['iPhone 12'] }, }, ], // تشغيل خادم التطوير المحلي قبل الاختبارات webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });

كتابة أول اختبار لك

أنشئ ملف اختبار في دليل tests/:

// tests/example.spec.ts import { test, expect } from '@playwright/test'; test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); // توقع أن يحتوي العنوان على سلسلة فرعية await expect(page).toHaveTitle(/Playwright/); }); test('get started link', async ({ page }) => { await page.goto('https://playwright.dev/'); // انقر على رابط البدء await page.getByRole('link', { name: 'Get started' }).click(); // يتوقع أن تحتوي الصفحة على عنوان باسم التثبيت await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); });

تشغيل الاختبارات

# تشغيل جميع الاختبارات npx playwright test # تشغيل الاختبارات في وضع برأس (رؤية المتصفح) npx playwright test --headed # تشغيل الاختبارات في متصفح محدد npx playwright test --project=firefox # تشغيل ملف اختبار محدد npx playwright test tests/example.spec.ts # تشغيل الاختبارات في وضع التصحيح npx playwright test --debug # عرض تقرير الاختبار npx playwright show-report

المحددات: العثور على العناصر

يوفر Playwright استراتيجيات محدد قوية:

// حسب الدور (مفضل - صديق لإمكانية الوصول) await page.getByRole('button', { name: 'Submit' }); await page.getByRole('heading', { level: 1 }); await page.getByRole('textbox', { name: 'Email' }); await page.getByRole('checkbox', { name: 'I agree' }); // حسب نص التسمية (لمدخلات النماذج) await page.getByLabel('Email address'); // حسب العنصر النائب await page.getByPlaceholder('Enter your email'); // حسب محتوى النص await page.getByText('Welcome back'); await page.getByText(/sign in/i); // تعبير عادي، غير حساس لحالة الأحرف // حسب معرف الاختبار await page.getByTestId('submit-button'); // حسب محدد CSS await page.locator('.btn-primary'); await page.locator('#username'); // دمج المحددات await page.locator('form').getByRole('button', { name: 'Submit' }); // الترشيح await page.getByRole('listitem').filter({ hasText: 'Product 1' }); // العنصر الترتيبي await page.getByRole('listitem').nth(2);
نصيحة: محددات Playwright صارمة افتراضياً. إذا تطابقت عناصر متعددة، فسيفشل الاختبار. استخدم first() أو last() أو nth() عند التحديد عمداً من تطابقات متعددة.

التفاعلات

تنفيذ إجراءات المستخدم:

// النقر await page.getByRole('button', { name: 'Submit' }).click(); // النقر المزدوج await page.getByText('Edit').dblclick(); // النقر بزر الماوس الأيمن await page.getByText('File').click({ button: 'right' }); // الكتابة await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').type('password123'); // يكتب حرفاً بحرف // الضغط على مفاتيح لوحة المفاتيح await page.getByLabel('Search').press('Enter'); await page.keyboard.press('Control+A'); // الاختيار من القائمة المنسدلة await page.getByLabel('Country').selectOption('United States'); await page.getByLabel('Country').selectOption({ value: 'us' }); // التحديد/إلغاء التحديد await page.getByLabel('I agree').check(); await page.getByLabel('Subscribe').uncheck(); // رفع الملف await page.getByLabel('Upload').setInputFiles('path/to/file.pdf'); await page.getByLabel('Photos').setInputFiles(['1.jpg', '2.jpg']); // ملفات متعددة // التحويم await page.getByText('Tooltip').hover(); // السحب والإفلات await page.getByText('Item 1').dragTo(page.getByText('Drop Zone'));

التأكيدات

يوفر Playwright تأكيدات إعادة المحاولة التلقائية:

// الرؤية await expect(page.getByText('Welcome')).toBeVisible(); await expect(page.getByText('Loading')).toBeHidden(); // محتوى النص await expect(page.getByRole('heading')).toHaveText('Dashboard'); await expect(page.getByRole('alert')).toContainText('Error occurred'); // القيمة await expect(page.getByLabel('Email')).toHaveValue('user@example.com'); // السمات await expect(page.getByRole('link')).toHaveAttribute('href', '/about'); await expect(page.getByRole('button')).toHaveClass(/btn-primary/); // العد await expect(page.getByRole('listitem')).toHaveCount(5); // URL await expect(page).toHaveURL(/.*dashboard/); await expect(page).toHaveTitle('Dashboard'); // ممكّن/معطّل await expect(page.getByRole('button')).toBeEnabled(); await expect(page.getByRole('button')).toBeDisabled(); // محدد await expect(page.getByLabel('Terms')).toBeChecked(); await expect(page.getByLabel('Newsletter')).not.toBeChecked(); // مقارنة لقطة الشاشة await expect(page).toHaveScreenshot('homepage.png');

الانتظار التلقائي

ينتظر Playwright تلقائياً حتى تصبح العناصر قابلة للتنفيذ:

test('auto-waiting example', async ({ page }) => { await page.goto('/'); // ينتظر Playwright أن يكون الزر: // - مرفقاً بـ DOM // - مرئياً // - مستقراً (لا يتحرك) // - ممكّناً // - غير مغطى بعناصر أخرى await page.getByRole('button', { name: 'Submit' }).click(); // انتظر الشروط المحددة await page.getByText('Loading').waitFor({ state: 'hidden' }); await page.getByText('Success').waitFor({ state: 'visible' }); // انتظر تغيير URL await page.waitForURL('**/dashboard'); // انتظر طلب الشبكة await page.waitForResponse(response => response.url().includes('/api/users') && response.status() === 200 ); // مهلة مخصصة await page.getByText('Slow Element').click({ timeout: 30000 }); });
ملاحظة: الانتظار التلقائي لـ Playwright يزيل معظم الاختبارات المتقلبة. نادراً ما تكون الانتظارات الصريحة مطلوبة.

اعتراض الشبكة

محاكاة وتعديل طلبات الشبكة:

test('mock API response', async ({ page }) => { // محاكاة استجابة API await page.route('**/api/users', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' } ]) }); }); await page.goto('/users'); await expect(page.getByText('John Doe')).toBeVisible(); await expect(page.getByText('Jane Smith')).toBeVisible(); }); test('modify API response', async ({ page }) => { await page.route('**/api/user', async route => { // جلب الاستجابة الأصلية const response = await route.fetch(); const json = await response.json(); // تعديل الاستجابة json.name = 'Modified Name'; // الإيفاء بالاستجابة المعدلة await route.fulfill({ response, json }); }); await page.goto('/profile'); await expect(page.getByText('Modified Name')).toBeVisible(); }); test('abort requests', async ({ page }) => { // حظر طلبات التحليلات await page.route('**/analytics/**', route => route.abort()); await page.goto('/'); });

نموذج كائن الصفحة

تنظيم الاختبارات مع نمط كائن الصفحة:

// pages/LoginPage.ts import { Page, Locator } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Login' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } async expectErrorMessage(message: string) { await expect(this.errorMessage).toContainText(message); } }

استخدام كائن الصفحة في الاختبارات:

// tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test('successful login', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'password123'); await expect(page).toHaveURL(/.*dashboard/); }); test('failed login shows error', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid@example.com', 'wrong'); await loginPage.expectErrorMessage('Invalid credentials'); });

التركيبات لإعداد الاختبار

استخدم التركيبات لإعداد متطلبات الاختبار المسبقة:

// fixtures/auth.ts import { test as base } from '@playwright/test'; export const test = base.extend({ // تركيبة صفحة معتمدة authenticatedPage: async ({ page }, use) => { // انتقل إلى تسجيل الدخول await page.goto('/login'); // تنفيذ تسجيل الدخول await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('password123'); await page.getByRole('button', { name: 'Login' }).click(); // انتظر المصادقة await page.waitForURL('**/dashboard'); // توفير صفحة معتمدة للاختبار await use(page); // التنظيف (تسجيل الخروج) await page.getByRole('button', { name: 'Logout' }).click(); } }); export { expect } from '@playwright/test';

استخدم التركيبة في الاختبارات:

// tests/dashboard.spec.ts import { test, expect } from '../fixtures/auth'; test('displays user dashboard', async ({ authenticatedPage }) => { // تم تسجيل الدخول بالفعل عبر التركيبة await expect(authenticatedPage.getByText('Welcome back')).toBeVisible(); });

الاختبار عبر المتصفحات

تشغيل الاختبارات عبر متصفحات متعددة:

// التشغيل على جميع المتصفحات المكونة npx playwright test // التشغيل على متصفح محدد npx playwright test --project=firefox npx playwright test --project=webkit // التشغيل على متصفحات محددة متعددة npx playwright test --project=chromium --project=firefox

محاكاة الهاتف المحمول

اختبر منافذ عرض الهاتف المحمول وتفاعلات اللمس:

test('mobile menu', async ({ page }) => { // محاكاة iPhone 12 await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); // يجب أن تكون قائمة الهاتف المحمول مرئية await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible(); // انقر على قائمة الهاتف المحمول await page.getByRole('button', { name: 'Menu' }).tap(); // يجب أن تظهر عناصر القائمة await expect(page.getByRole('navigation')).toBeVisible(); });

تصحيح الاختبارات

يوفر Playwright أدوات تصحيح قوية:

# تشغيل الاختبارات في وضع التصحيح (يفتح Playwright Inspector) npx playwright test --debug # تصحيح اختبار محدد npx playwright test tests/example.spec.ts --debug # التشغيل في وضع برأس لرؤية المتصفح npx playwright test --headed # إبطاء التنفيذ npx playwright test --headed --slow-mo=1000

استخدم مساعدات التصحيح في الكود:

test('debugging example', async ({ page }) => { await page.goto('/'); // إيقاف التنفيذ (يفتح Playwright Inspector) await page.pause(); // التقاط لقطة شاشة await page.screenshot({ path: 'screenshot.png' }); // الحصول على محتوى HTML للتصحيح const html = await page.content(); console.log(html); // تقييم JavaScript const title = await page.evaluate(() => document.title); console.log(title); });

عارض التتبع

تسجيل وتحليل تنفيذ الاختبار:

# تسجيل الأثر npx playwright test --trace on # عرض الأثر npx playwright show-trace trace.zip

يعرض عارض التتبع:

  • الجدول الزمني الكامل لتنفيذ الاختبار
  • لقطات الشاشة في كل خطوة
  • طلبات الشبكة
  • سجلات وحدة التحكم
  • لقطات DOM
  • تفاصيل الإجراء
تمرين:

أنشئ مجموعة اختبارات شاملة لتطبيق مدونة باستخدام Playwright:

  1. أنشئ كائنات صفحة لـ: الصفحة الرئيسية، صفحة تسجيل الدخول، صفحة منشور المدونة، صفحة المسؤول
  2. قم بتنفيذ تركيبة المصادقة لمستخدم المسؤول
  3. اختبر تدفقات المستخدم:
    • تصفح منشورات المدونة (الترقيم، الترشيح)
    • قراءة منشور المدونة الكامل
    • إرسال تعليق على منشور المدونة
    • تسجيل دخول المسؤول
    • إنشاء منشور مدونة جديد (مع رفع الصورة)
    • تحرير منشور مدونة موجود
    • حذف منشور المدونة
  4. اختبر عبر Chromium و Firefox و WebKit
  5. اختبر منفذ عرض الهاتف المحمول (قائمة الهامبورجر، الصور المتجاوبة)
  6. محاكاة استجابات API لمنشورات المدونة
  7. تحقق من لقطة الشاشة لبطاقات منشور المدونة

أفضل الممارسات

  • استخدم محددات قائمة على الأدوار: أعط الأولوية لـ getByRole() لإمكانية الوصول والاستقرار
  • نموذج كائن الصفحة: احصر تفاعلات الصفحة في فئات قابلة لإعادة الاستخدام
  • عزل الاختبار: يجب أن يكون كل اختبار مستقلاً ولا يعتمد على الآخرين
  • استخدم التركيبات: قم بإعداد متطلبات الاختبار المسبقة المشتركة بالتركيبات
  • محاكاة التبعيات الخارجية: محاكاة واجهات برمجة التطبيقات والخدمات الخارجية
  • استفد من الانتظار التلقائي: ثق بالانتظار التلقائي لـ Playwright، تجنب الانتظارات اليدوية
  • اختبر عبر المتصفحات: قم بتشغيل الاختبارات على Chromium و Firefox و WebKit
  • استخدم عارض التتبع: قم بتمكين التتبع لتصحيح الاختبارات الفاشلة
أخطاء شائعة:
  • عدم استخدام تسلسل المحدد - استخدم دائماً المحدد الأكثر تحديداً
  • استخدام waitForTimeout() بدلاً من الاعتماد على الانتظار التلقائي
  • عدم الاختبار عبر متصفحات متعددة
  • إنشاء اختبارات مترابطة تفشل عند التشغيل بشكل منفصل
  • عدم استخدام نموذج كائن الصفحة للتطبيقات المعقدة
  • اختيار العناصر عن طريق النص الذي يتغير مع الترجمة

Playwright مقابل Cypress

الاختلافات الرئيسية لمساعدتك في الاختيار:

الميزة Playwright Cypress
دعم المتصفح Chromium، Firefox، WebKit قائم على Chromium، Firefox
اختبار الهاتف المحمول محاكاة الجهاز الأصلية محاكاة منفذ العرض
مشغل الاختبار تنفيذ متوازٍ مدمج تسلسلي (متوازٍ في النسخة المدفوعة)
API قائم على الوعود (async/await) قائمة انتظار الأوامر (التسلسل)
تجربة المطور أدوات ممتازة، آثار تصحيح ممتاز، السفر عبر الزمن

في الدرس التالي، سنستكشف تقنيات وأدوات اختبار API لاختبار خدمات الخلفية.