تشغيل الاختبارات في خطوط 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المحفّزات:
onطلبات الدمج والدفع إلىmainوجدولة cron ليلية بتوقيت UTC تبدأ مسار العمل. - 2شرط المهمةتعمل مهمة
smokeلطلبات الدمج والدفع، ولا تعمل للجدولة أبداً. - 3المشغّل (runner)آلة افتراضية جديدة لكل مهمة؛ لا يبقى بعدها إلا الذاكرة المؤقتة والمخرجات.
- 4الخطواتتعمل من الأعلى إلى الأسفل في مساحة عمل واحدة؛ ورمز الخروج غير الصفري يُفشل المهمة.
- 5الذاكرة المؤقتة للاعتمادياتتستعيد تنزيلات npm ما دام
package-lock.jsonلم يتغير. - 6اختيار طلب الدمجاختبارات
@smokeفقط في Chromium: سريعة بما يكفي لتحجب الدمج. - 7احتفظ بالأدلةيعمل الرفع حتى بعد خطوة اختبار فاشلة.
- 8مصفوفة المتصفحاتمهمة متوازية لكل متصفح؛ و
fail-fast: falseيترك الثلاثة تكمل. - 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تقارير الاختبار والمخرجات والمهام المتوازية
اضبط مشغّل الاختبار ليكتب نتائج للآلات والناس، ثم اجعل المهمة تحتفظ بها. المقتطف أدناه يكتب 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'] } },
],
});أخطاء شائعة
- الإزاحة بعلامات الجدولة، وهي مرفوضة في 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/- ضع الحزم في جدول مقابل المحفّزات الثلاثة: تحجب الدمج، أو للإبلاغ فقط، أو لا تعمل.
- افحص المحفّزات ووقت cron وشروط المهام.
- افحص التخزين المؤقت والمصفوفة والمخرجات.
إظهار الإجابة النموذجية
بصيغته الحالية ينتظر كل طلب دمج نحو 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 documentationكيف تُعرَّف مسارات العمل والمهام والمشغّلات والمخرجات في GitHub Actions — حيث تعمل الاختبارات الآلية عند كل دفع أو طلب دمج.
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- YAML Ain't Markup Language (YAML™) version 1.2.2نموذج بيانات YAML 1.2 وصياغته وعلاقته بـ JSON (كل مستند JSON هو YAML 1.2 صالح).
- Unit test reports — GitLab CI/CDنشر نتائج JUnit XML كمخرجات لخط الأنابيب لتظهر الاختبارات الفاشلة في طلب الدمج ويمكن مقارنتها بين التشغيلات.
تدرّب بأدوات متقن
تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.
تحويل مستندات YAML إلى JSON مع الإبلاغ عن الأخطاء.
ترجم أي تعبير cron إلى لغة إنجليزية مبسّطة واعرض أوقات التشغيل القادمة.
أنشئ تعابير cron بصرياً وعاين أوقات التشغيل التالية.
تنسيق XML بشكل أنيق مع مسافة بادئة قابلة للتخصيص، وإعادة تصغيره عند الحاجة.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.