EduFlow
اختبار تطبيقات JavaScript
⁦0%⁩

الوحدة 4 · اختبارات end-to-end مع Playwright

الـ Fixtures وعزل الاختبارات

20 دقراءة 4 داملأ الفراغات

الدرس في لمحة

تُبقي اختبارات الـ end-to-end مستقلّة باستخدام سياقات المتصفّح المعزولة في Playwright، وfixtures مخصّصة تنشئ بياناتها وتنظّفها، وحالة تسجيل دخول محفوظة، وتوجيه انتقائي لطلبات الشبكة.

  1. 1سياق جديد لكل اختبار
  2. 2الـ Fixtures: التهيئة والتنظيف في مكان واحد
  3. 3تسجيل الدخول مرة واحدة
  4. 4التوجيه: التحكّم في ما يقوله الخادم

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

أشهر سبب لفقدان الثقة في مجموعة end-to-end ليس التوقيت؛ بل الحالة المشتركة. ينجح الاختبار 12 وحده ويفشل في التشغيل الكامل لأن الاختبار 7 أعاد تسمية المجموعة التي يستخدمها. أو ينجح اختبار في أيام الاثنين فقط لأن البيانات الأولية تحتوي مصروفًا لـ “هذا الأسبوع”. العزل هو العلاج، وPlaywright تعطيك معظمه مجانًا.

سياق جديد لكل اختبار

يستلم كل اختبار سياق متصفّح (browser context) خاصًّا به: ملفًّا شخصيًا يشبه وضع التصفّح الخفي، له ملفات تعريف الارتباط وlocalStorage وsessionStorage والذاكرة المؤقّتة الخاصة به. فتح سياق رخيص (أجزاء من الثانية، لا متصفّح جديد)، لذا تنشئ Playwright سياقًا جديدًا لكل اختبار. ولهذا تستطيع التشغيل مع fullyParallel: true عبر عدة عمّال (workers): فلا يستطيع اختباران رؤية تسجيلات دخول بعضهما أو المسوّدات المحفوظة.

جهة المتصفّح معزولة. أما الـ backend لديك فليس كذلك. إن عدّل اختباران المجموعة نفسها على الخادم نفسه، يتصادمان. وبقية هذا الدرس عن عزل البيانات.

الـ Fixtures: التهيئة والتنظيف في مكان واحد

لقد كنت تستخدم الـ fixtures أصلًا: فـ page في async ({ page }) => … واحدة منها. وتأتي Playwright أيضًا بـ context وbrowser وbrowserName وrequest (عميل HTTP يشترك في baseURL). وتعرّف fixtures خاصة بك عبر test.extend:

e2e/fixtures.ts
import { test as base, expect } from '@playwright/test';

type Group = { id: string; name: string };

export const test = base.extend<{ group: Group }>({
  group: async ({ request }, use) => {
    // Setup: create a fresh group through the API
    const response = await request.post('/api/groups', {
      data: { name: `Trip ${crypto.randomUUID().slice(0, 8)}`, members: ['Ana', 'Ben'] },
    });
    expect(response.ok()).toBeTruthy();
    const group: Group = await response.json();

    await use(group); // the test runs here

    // Teardown: runs after the test, pass or fail
    await request.delete(`/api/groups/${group.id}`);
  },
});

export { expect };
e2e/settle-up.spec.ts
import { test, expect } from './fixtures';

test('settling up clears every balance', async ({ page, group }) => {
  await page.goto(`/groups/${group.id}`);
  // …add an expense, click Settle up…
  await expect(page.getByRole('status')).toHaveText('Everyone is settled up');
});

ثلاث خصائص تجعل هذا أفضل من beforeEach:

  • عند الطلب. لا ينشئ مجموعةً إلا الاختبارات التي تذكر group في معاملاتها. والاختبارات التي لا تحتاجها لا تدفع شيئًا.
  • التهيئة والهدم يعيشان معًا، حول await use(...)، فلا يمكن أن يخرج التنظيف عن التزامن مع التهيئة.
  • قابلة للتركيب. يمكن لـ fixture الـ group أن تعتمد على request؛ ويمكن لـ fixture expense أن تعتمد على group. وتحلّ Playwright الترتيب.

إنشاء البيانات عبر الـ API بدل النقر في الواجهة يُبقي كل اختبار مركّزًا على مسار واحد ويوفّر ثوانيَ في كل اختبار. اختبار واحد ينقر عبر “إنشاء مجموعة”؛ والخمسون الآخرون يحصلون على مجموعة من الـ fixture.

تسجيل الدخول مرة واحدة

إن كان Splitwise-lite يتطلّب تسجيل الدخول، فالتنقّل في النموذج في كل اختبار يضيّع الوقت ويُرهق خادم المصادقة. نمط Playwright هو setup project (مشروع تهيئة): اختبار واحد يسجّل الدخول ويحفظ حالة تخزين المتصفّح في ملف، والمشاريع الحقيقية تعلن اعتمادها عليه وتبدأ وقد سُجّل دخولها مسبقًا:

playwright.config.ts
// …inside defineConfig
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

ينتهي اختبار التهيئة بـ await page.context().storageState({ path: 'playwright/.auth/user.json' }). أضف playwright/.auth إلى .gitignore: فهو يحتوي ملفات تعريف ارتباط لجلسات حقيقية.

التوجيه: التحكّم في ما يقوله الخادم

بعض الحالات صعبة الإنتاج مع backend حقيقي: 500 عند الحفظ، أو استجابة بطيئة، أو مجموعة فارغة من حساب قديم. يعترض page.route الطلبات القادمة من الصفحة ويتيح لك الإجابة عنها:

TypeScript
test('explains a failed save', async ({ page, group }) => {
  await page.route('**/api/expenses', (route) =>
    route.fulfill({ status: 500, json: { error: 'Database unavailable' } }),
  );

  await page.goto(`/groups/${group.id}`);
  await page.getByLabel('Description').fill('Dinner');
  await page.getByLabel('Amount').fill('30');
  await page.getByRole('button', { name: 'Add expense' }).click();

  await expect(page.getByRole('alert')).toHaveText('Could not save the expense. Try again.');
});

استخدم التوجيه للنادر والمعطوب. وإن وجدت نفسك توجّه كل طلب، فقد أعدت بناء اختبار مكوّن بأداة أبطأ؛ دع المسارات السعيدة تصل إلى الـ backend الحقيقي، لأن هذا التكامل هو سبب وجود اختبارات الـ end-to-end.

ويجعل العزل أيضًا تتبّع حالات الفشل أرخص. حين يفشل اختبار، تستطيع إعادة تشغيل ذلك الاختبار وحده بـ npx playwright test -g "settling up" والحصول على النتيجة نفسها، لأن لا شيء مما يعتمد عليه يعيش خارجه. أما مع الاعتماديات الخفيّة، فتنجح إعادة التشغيل المنفردة ويفشل التشغيل الكامل، وتبقى تقسّم المجموعة نصفين بعد نصفين لتجد أيّ جار كسره.

في التمرين تُكمل fixture تنشئ مجموعة وتحذفها. الدرس القادم: ماذا تفعل حين يفشل اختبار رغم كل ذلك.

أهم النقاط

  • يحصل كل اختبار في Playwright على سياق متصفّح جديد (له ملفات تعريف الارتباط والتخزين والذاكرة المؤقّتة الخاصة به)، فتستطيع الاختبارات أن تعمل بالتوازي وبأي ترتيب.
  • الـ fixture (تجهيزة الاختبار) التي تُنشأ بـ test.extend تُعدّ شيئًا ما، وتسلّمه للاختبار عبر use()، ثم تهدمه بعد ذلك، وذلك للاختبارات التي تطلبها فقط.
  • يجب أن ينشئ كل اختبار البيانات التي يحتاجها (ويُفضَّل عبر الـ API) بدل الاعتماد على بيانات أولية مشتركة أو على أن اختبارًا آخر عمل قبله.
  • سجّل الدخول مرة واحدة في setup project، واحفظ حالة التخزين، وأعد استخدامها، بدل التنقّل في نموذج تسجيل الدخول في كل اختبار.
  • استخدم page.route لمحاكاة استجابات الخادم النادرة، والـ backend الحقيقي للمسارات السعيدة.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

ينشئ الاختبار A مجموعة اسمها “Lisbon trip”؛ ويفتح الاختبار B المجموعة “Lisbon trip” ويفحص أرصدتها. ينجح B محليًا ويفشل في CI. ما أصل المشكلة؟

السؤال 2 من 3

في الـ fixture، ماذا يفعل الكود الواقع بعد await use(group)؟

السؤال 3 من 3

ما أفضل استخدام لـ page.route في مجموعة end-to-end؟

تدريب

املأ الفراغات

أكمل fixture للمجموعة

أكمل هذه الـ fixture في Playwright كي يحصل كل اختبار يطلب group على مجموعة جديدة تُنشأ عبر الـ API، وكي تُحذف المجموعة بعد الاختبار، نجح أو فشل. ثم أكمل الاختبار كي يستلم الـ fixture ويستخدم assertion موجّهًا للويب.

import { test as base, expect } from '@playwright/test'; type Group = { id: string; name: string }; export const test = base.<{ group: Group }>({ group: async ({ }, use) => { const response = await request.post('/api/groups', { data: { name: `Trip ${crypto.randomUUID().slice(0, 8)}` }, }); const group: Group = await response.json(); await (group); await request.(`/api/groups/${group.id}`); }, }); test('a new group starts settled up', async ({ page, }) => { await page.goto(`/groups/${group.id}`); await (page.getByRole('status')).toHaveText('Everyone is settled up'); });