تشغيل الاختبارات في خطوط CI

تشغيل الاختبارات في خط أنابيب CI يعني أن تصف، في ملف يقرؤه خادم CI، متى تعمل الاختبارات، وعلى أي أجهزة، وبأي اعتماديات، وماذا تترك خلفها. في GitHub Actions هذا الملف مسار عمل (workflow) بصيغة YAML داخل .github/workflows/؛ ويعبّر GitLab CI (.gitlab-ci.yml) وJenkins (Jenkinsfile) عن الأفكار نفسها. فمسار عمل واحد يمكنه مثلاً أن يشغّل 25 اختبار متصفح موسومة @smoke في Chromium عند كل طلب دمج (pull request)، والاختبارات الـ180 كلها في ثلاثة متصفحات كل ليلة.

عرض دليل «الاختبار في خطوط CI/CD» ما ينتجه خط الأنابيب: نتائج بصيغة JUnit XML، ومخرجات (artifacts) مثل التتبعات ولقطات الشاشة، وحزماً مقسّمة إلى أجزاء (shards) متوازية. وهذا الدرس هو الجانب العملي: الملف الذي يجعل ذلك يحدث.

لماذا يهم

في فريق يعمل بـCI يشغّل خط الأنابيب معظم الاختبارات، فمسار العمل هو الذي يقرر أي أدلة ستوجد. والفحص الذي يشغّل المجموعة الخطأ، أو حزمة المتصفح التي لا تعمل إلا في Chromium، يظهر أخضر مع ذلك؛ والفحص الذي يستغرق 40 دقيقة يتوقف الناس عن انتظاره.

والمختبِر هو غالباً من يقرر ما تغطيه الحزمة، لذا يحتاج إلى تعديل مسار العمل بنفسه: إضافة متصفح، أو نقل حزمة بطيئة إلى التشغيل الليلي، أو الاحتفاظ بتقرير المهمة الفاشلة.

تشريح ملف خط الأنابيب

لـYAML ثلاث لبنات. الربط (mapping) هو key: value؛ والقائمة (list) أسطر تبدأ بـ- ، أو الصيغة المختصرة [chromium, firefox]؛ والتداخل لا يظهر إلا بالإزاحة (indentation) بالمسافات. وعلامات الجدولة (tabs) غير مسموحة في الإزاحة، ومسافة واحدة زائدة أو ناقصة قد تنقل المفتاح إلى كتلة أخرى. وما بعد # تعليق، والقيمة التي تبدأ بـ* يجب وضعها بين علامتي تنصيص، كما في تعبير cron هذا: '*/30 * * * *'.

يحدد المفتاح on المحفّزات (triggers): pull_request عند فتح طلب دمج أو إعادة فتحه أو إضافة إيداعات (commits) جديدة إليه، وpush مقصوراً على main، وschedule مع تعبير cron. وحقوله الخمسة هي الدقيقة والساعة واليوم من الشهر والشهر واليوم من الأسبوع، فالتعبير '0 1 * * *' يعني الساعة ⁦01:00⁩ كل يوم؛ وتعمل الجدولة على الفرع الافتراضي، وبتوقيت UTC ما لم تُحدَّد لها منطقة زمنية بالمفتاح timezone. وكل مهمة (job) تحت jobs تعمل على مشغّل (runner)، وهو الجهاز الذي يسمّيه runs-on؛ والمشغّل الذي تستضيفه GitHub آلة افتراضية جديدة لكل مهمة، فلا يبقى شيء بين المهام إلا ما تخزّنه مؤقتاً أو ترفعه. وخطوات المهمة steps تعمل بالترتيب في مساحة عمل واحدة: uses يشغّل إجراءً منشوراً (action)، وrun ينفّذ أمر shell، ورمز الخروج غير الصفري يُفشل المهمة.

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

.github/workflows/e2e.yml: مسار عمل واحد ومهمتان

# .github/workflows/e2e.yml
name: e2e

الجزء 1:on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 1 * * *'          # every night, 01:00 UTC

jobs:
  smoke:
    الجزء 2:if: github.event_name != 'schedule'
    الجزء 3:runs-on: ubuntu-latest
    timeout-minutes: 15
    الجزء 4:steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          الجزء 5:cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --project=chromium الجزء 6:--grep @smoke
      - uses: actions/upload-artifact@v7
        الجزء 7:if: always()
        with:
          name: smoke-results
          path: results/
          retention-days: 14

  nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    timeout-minutes: 45
    الجزء 8:strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          الجزء 9:name: nightly-${{ matrix.browser }}
          path: results/
          retention-days: 14
  1. 1المحفّزات: onطلبات الدمج والدفع إلى main وجدولة cron ليلية بتوقيت UTC تبدأ مسار العمل.
  2. 2شرط المهمةتعمل مهمة smoke لطلبات الدمج والدفع، ولا تعمل للجدولة أبداً.
  3. 3المشغّل (runner)آلة افتراضية جديدة لكل مهمة؛ لا يبقى بعدها إلا الذاكرة المؤقتة والمخرجات.
  4. 4الخطواتتعمل من الأعلى إلى الأسفل في مساحة عمل واحدة؛ ورمز الخروج غير الصفري يُفشل المهمة.
  5. 5الذاكرة المؤقتة للاعتمادياتتستعيد تنزيلات npm ما دام package-lock.json لم يتغير.
  6. 6اختيار طلب الدمجاختبارات @smoke فقط في Chromium: سريعة بما يكفي لتحجب الدمج.
  7. 7احتفظ بالأدلةيعمل الرفع حتى بعد خطوة اختبار فاشلة.
  8. 8مصفوفة المتصفحاتمهمة متوازية لكل متصفح؛ وfail-fast: false يترك الثلاثة تكمل.
  9. 9اسم فريد للمخرجاتكل مهمة في المصفوفة ترفع باسمها الخاص؛ والاسم المكرر في التشغيل نفسه يفشل.

بصيغة GitHub Actions. والأجزاء نفسها موجودة في GitLab CI وJenkins بأسماء أخرى.

النسخة النصية
# .github/workflows/e2e.yml
name: e2e

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 1 * * *'          # every night, 01:00 UTC

jobs:
  smoke:
    if: github.event_name != 'schedule'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --project=chromium --grep @smoke
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: smoke-results
          path: results/
          retention-days: 14

  nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: nightly-${{ matrix.browser }}
          path: results/
          retention-days: 14

جرّبها: منسّق YAML جرّبها: محوّل YAML إلى JSON جرّبها: شارح تعابير cron

التخزين المؤقت للاعتماديات ومصفوفة المتصفحات

لأن كل مهمة تبدأ نظيفة، سيحمّل npm ci كل الحزم في كل تشغيل. والذاكرة المؤقتة للاعتماديات (dependency cache) تحفظ هذه التنزيلات تحت مفتاح محسوب من ملف القفل (lock file) وتستعيدها في المرة التالية؛ وحين يتغير package-lock.json يتغير المفتاح. ويخزّن cache: npm في actions/setup-node ذاكرة تنزيلات npm لا مجلد node_modules، فيبقى npm ci يثبّت بالضبط ما يقوله ملف القفل، لكن أسرع، ويجب أن يعطي النتيجة نفسها حين لا توجد الذاكرة المؤقتة. وتنصح وثائق Playwright الخاصة بـCI بعدم التخزين المؤقت لملفات المتصفحات، لأن استعادتها تستغرق تقريباً وقت تنزيلها.

المصفوفة (matrix) تحوّل تعريف مهمة واحداً إلى مهمة لكل تركيبة من القيم. فالسطر browser: [chromium, firefox, webkit] ينشئ ثلاث مهام متوازية، تمرّر كل منها قيمتها إلى --project، ويجب أن تطابق القيمة اسم مشروع في إعدادات Playwright. وإضافة shard: [1, 2] تجعلها ست مهام، فلا تضف بُعداً إلا لخطر حقيقي، مثل اختلاف العرض بين المحرّكات. ويترك fail-fast: false كل متصفح يكمل، فيظهر هل الإخفاق خاص بمحرّك واحد أم عام.

مثال تطبيقي: طلب الدمج أم التشغيل الليلي

لدى فريق متجر إلكتروني ⁦1,240⁩ اختبار وحدة و310 اختبارات API و180 اختبار متصفح، منها 25 موسومة @smoke لتسجيل الدخول والبحث والسلة والدفع. وتستغرق حزمة المتصفح نحو 35 دقيقة لكل متصفح، وميزانية طلب الدمج 15 دقيقة. يفرز الفريق كل حزمة حسب سرعتها وثباتها والخطر الذي تغطيه ومن يتصرف حين تفشل. ومسار العمل أعلاه ينفّذ صفوف المتصفح.

  • طلبات الدمج تحصل على فحوص سريعة وثابتة لأهم الرحلات؛ ينتظرها صاحب التغيير، لذا فهي بوابة (gate) تحجب الدمج حتى تنجح.
  • الدفع إلى main يكرر المجموعة نفسها: طلبا دمج نجح كل منهما وحده قد ينكسران معاً.
  • التشغيل الليلي يحمل الحزم البطيئة والواسعة؛ ويقرؤه مالك محدد كل صباح ويفحص الإيداعات منذ آخر ليلة خضراء.
  • الاختبارات المتذبذبة المحجورة تخرج من مجموعة طلب الدمج وتعمل ليلاً فقط، فتُبلغ دون أن تعطّل أحداً. ففي المهمة الليلية تضيف خطوة الاختبار الرئيسية الخيار --grep-invert @quarantine فتتخطى الاختبارات الموسومة به، وتشغّل خطوة ثانية الاختبارات المحجورة بـ--grep @quarantine مع continue-on-error: true، فلا يُفشل فشلُها المهمة، ومع if: always()، فتعمل حتى بعد فشل الخطوة الرئيسية.
Suite                 Tests    Time*   Pull request  Push to main  Nightly 01:00 UTC
Lint + unit           1,240    3 min   gate          run           run
API tests             310      6 min   gate          run           run
E2E @smoke, Chromium  25       4 min   gate          run           -
E2E full, 3 browsers  3 x 180  35 min  -             -             run, 3 parallel jobs

gate: must pass before the merge
*  wall-clock time per job; illustrative figures

جرّبها: مولّد تعابير cron

تقارير الاختبار والمخرجات والمهام المتوازية

اضبط مشغّل الاختبار ليكتب نتائج للآلات والناس، ثم اجعل المهمة تحتفظ بها. المقتطف أدناه يكتب JUnit XML لخادم CI، وتقرير HTML للناس، والتتبعات ولقطات الشاشة للاختبارات الفاشلة. يقرأ GitLab ملف XML عبر artifacts:reports:junit، ويقرؤه Jenkins عبر الخطوة junit من إضافة JUnit؛ وفي غيرهما راجع وثائق المنصة.

المخرجات (artifacts) ملفات ترفعها المهمة لتبقى بعد زوال مشغّلها. ارفعها مع if: always(): فالخطوة التي تلي خطوة فاشلة تُتخطى افتراضياً، وأنت تحتاج إليها في التشغيلات الفاشلة تحديداً. ويشترط GitHub Actions أسماء مخرجات فريدة داخل التشغيل الواحد، ومن هنا nightly-${{ matrix.browser }}؛ ويحدد retention-days مدة بقائها. وأبقِ البيانات الشخصية والأسرار خارجها.

المهام التي ليس فيها needs تعمل بالتوازي، وكذلك مهام المصفوفة، بقدر ما تسمح المشغّلات؛ ويضع max-parallel حداً للمصفوفة. وداخل المهمة تفضّل إرشادات Playwright الخاصة بـCI عاملاً (worker) واحداً لتشغيل ثابت، والتوسع بالأجزاء (shards) بدلاً من ذلك. وادمج تقارير المهام، مثلاً بتقارير blob من Playwright والأمر npx playwright merge-reports، لترى البوابة نتيجة واحدة.

// playwright.config.ts (excerpt)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  workers: process.env.CI ? 1 : undefined,
  outputDir: 'results/artifacts',                       // traces, screenshots
  reporter: [
    ['junit', { outputFile: 'results/junit.xml' }],                // for the CI server
    ['html', { outputFolder: 'results/html', open: 'never' }],     // for people
  ],
  use: { trace: 'retain-on-failure', screenshot: 'only-on-failure' },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

جرّبها: منسّق XML

أخطاء شائعة

  • الإزاحة بعلامات الجدولة، وهي مرفوضة في YAML، أو بعدد خاطئ من المسافات، وقد يبقى الملف معه YAML صالحاً لكنه يعني شيئاً آخر. نسّق الملف واقرأه بصيغة JSON قبل الدفع.
  • رفع التقارير دون if: always()، فلا توجد المخرجات إلا للتشغيلات الخضراء.
  • تشغيل كل اختبار في كل متصفح عند كل طلب دمج حتى يدمج الناس دون انتظار.
  • تشغيل ليلي بلا مالك محدد: الليلة الحمراء التي تُقرأ بعد أسبوع ليست شبكة أمان.
  • الثقة بمهمة خضراء دون التحقق من عدد الاختبارات: فاختبار وُسم خطأً بـ@smoek يخرج بصمت من مجموعة --grep @smoke.
  • التخزين المؤقت لأي شيء يحدد النتيجة، مثل مخرجات الاختبار، أو مفتاح ذاكرة مؤقتة يتجاهل ملف القفل.

من منظور ضمان الجودة

راجع ملف مسار العمل في طلبات الدمج كما تراجع الكود. ومع كل تغيير اسأل: أي أحداث تشغّله، وأي اختبارات تعمل على طلب الدمج وأيها ليلاً فقط، وهل تحتفظ كل مهمة اختبار بتقاريرها عند الإخفاق، وهل ما زال الفحص ضمن الميزانية؟

سجّل التقسيم في استراتيجية الاختبار أو في README مع مالك التشغيل الليلي، وانقل الاختبارات بين الأعمدة كلما كبرت الحزمة. وحين يفشل تشغيل ليلي، اذكر في بلاغك التشغيلَ والمتصفحَ ونطاقَ الإيداعات الذي يجب فحصه.

تمرين عملي

راجع مسار عمل اختبار وقسّم حزمه

المطلوب

حزمة المتصفح لديك فيها 140 اختباراً: 20 موسومة @smoke، و110 اختبارات عادية، و10 اختبارات متذبذبة موسومة @quarantine. تستغرق مهمة المتصفح الواحد دقيقتين لمجموعة @smoke و28 دقيقة للكل، ويجب أن تنتهي فحوص طلب الدمج خلال 10 دقائق. وقد ظهر عيبان سابقان في WebKit فقط. والفريق يعمل في الرياض (⁦UTC+3⁩).

قرّر ما يعمل على طلبات الدمج، وعند الدفع إلى main، وليلاً، ثم راجع مسار العمل أدناه.

المادة
name: e2e
on:
  pull_request:
  schedule:
    - cron: '0 13 * * *'
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
      - uses: actions/upload-artifact@v7
        with:
          name: e2e-report
          path: results/
الخطوات
  1. ضع الحزم في جدول مقابل المحفّزات الثلاثة: تحجب الدمج، أو للإبلاغ فقط، أو لا تعمل.
  2. افحص المحفّزات ووقت cron وشروط المهام.
  3. افحص التخزين المؤقت والمصفوفة والمخرجات.
إظهار الإجابة النموذجية
الإجابة النموذجية

بصيغته الحالية ينتظر كل طلب دمج نحو 28 دقيقة ويحجب الدمج بسبب اختبارات محجورة. والبديل: طلبات الدمج والدفع إلى main تشغّل اختبارات @smoke في Chromium وWebKit، مهمتين متوازيتين نحو دقيقتين لكل منهما، وتحجب الدمج. والتشغيل الليلي يغطي الاختبارات الـ130 غير المحجورة في ثلاثة متصفحات (--grep-invert @quarantine)، ومعها المحجورة (--grep @quarantine) في خطوة عليها continue-on-error: true وif: always()، فتُبلغ دون أن تُفشل المهمة، وتعمل حتى بعد فشل الخطوة الرئيسية. ويقرأ مالك محدد التشغيل كل صباح.

أضف push مع branches: [main]. التعبير '0 13 * * *' يعني ⁦16:00⁩ في الرياض، أي في منتصف بعد الظهر لا ليلاً؛ أما '0 22 * * *' فيعمل في ⁦01:00⁩ بالتوقيت المحلي، وكذلك '0 1 * * *' مع timezone: Asia/Riyadh.

قسّمها إلى مهمة smoke (if: github.event_name != 'schedule'، والمصفوفة [chromium, webkit]، و--grep @smoke) ومهمة nightly (if: github.event_name == 'schedule'، والمتصفحات الثلاثة)؛ وأضف timeout-minutes، وfail-fast: false (وإلا ألغى فشلُ أول مهمة بقيةَ المتصفحات)، وcache: npm.

أضف if: always() إلى الرفع، وسمِّه باسم المهمة والمتصفح، مثل nightly-${{ matrix.browser }}: فثلاث مهام ترفع e2e-report تتصادم.

أسئلة شائعة

هل أحتاج إلى إتقان YAML للعمل مع خطوط الأنابيب؟

تحتاج إلى الأساسيات في هذا الدرس: الربط والقوائم والإزاحة والتنصيص. ومعظم التعديلات التي يجريها المختبِر على مسار العمل إضافة خطوة أو قيمة مصفوفة أو محفّز. تحقّق من الملف بأداة تنسيق YAML قبل الدفع، واقرأ مرجع الصياغة في المنصة لأي مفتاح لم تستخدمه من قبل.

هل يجب تشغيل اختبارات E2E (من البداية إلى النهاية) عند كل طلب دمج؟

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

لماذا لم يعمل مسار العمل المجدول في الوقت الذي توقعته؟

في GitHub Actions تعمل الجدولة على الفرع الافتراضي وبتوقيت UTC ما لم تُحدَّد لها منطقة زمنية بالمفتاح timezone، فالجدولة المضافة في فرع ميزة لا تفعل شيئاً حتى تُدمج، وقد يبعد الوقت ساعات عن ساعتك المحلية. وقد تتأخر التشغيلات المجدولة أو تُسقط حين يكون الضغط على المنصة عالياً، وبداية كل ساعة وقت مزدحم، فاختيار دقيقة مثل 17 بدل 0 يقلل احتمال التأخير. وفي المستودع العام تُعطَّل الجدولة بعد 60 يوماً بلا نشاط في المستودع.

ما الفرق بين الذاكرة المؤقتة والمخرجات؟

الذاكرة المؤقتة (cache) تحسين مشترك بين التشغيلات، مثل الاعتماديات المنزّلة بمفتاح من ملف القفل؛ وقد تغيب، ويجب أن تعمل المهمة رغم ذلك. أما المخرجات (artifacts) فنواتج تشغيل واحد، مثل التقارير والتتبعات ولقطات الشاشة، تُحفظ ليُنزّلها الناس ولتستخدمها مهام لاحقة.

المراجع

  • GitHub Actions documentationGitHubكيف تُعرَّف مسارات العمل والمهام والمشغّلات والمخرجات في GitHub Actions — حيث تعمل الاختبارات الآلية عند كل دفع أو طلب دمج.
  • Playwright DocumentationMicrosoft (Playwright)التوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
  • YAML Ain't Markup Language (YAML™) version 1.2.2YAML.org · 2021نموذج بيانات YAML 1.2 وصياغته وعلاقته بـ JSON (كل مستند JSON هو YAML 1.2 صالح).
  • Unit test reports — GitLab CI/CDGitLabنشر نتائج JUnit XML كمخرجات لخط الأنابيب لتظهر الاختبارات الفاشلة في طلب الدمج ويمكن مقارنتها بين التشغيلات.

تدرّب بأدوات متقن

تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.

التنسيق

تحويل مستندات YAML إلى JSON مع الإبلاغ عن الأخطاء.

JSON والبيانات

ترجم أي تعبير cron إلى لغة إنجليزية مبسّطة واعرض أوقات التشغيل القادمة.

أدوات مساعدة

أنشئ تعابير cron بصرياً وعاين أوقات التشغيل التالية.

أدوات مساعدة

تنسيق XML بشكل أنيق مع مسافة بادئة قابلة للتخصيص، وإعادة تصغيره عند الحاجة.

التنسيق

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة