عزل الاختبارات والتنفيذ المتوازي
عزل الاختبارات (Test Isolation) يعني أن يعطي الاختبار النتيجة نفسها سواء شُغّل وحده أو بأي ترتيب أو عدة مرات أو في اللحظة نفسها مع اختبارات أخرى، لأنه يبني الحالة التي يحتاجها ولا يعتمد على شيء يتركه اختبار آخر. والتنفيذ المتوازي (Parallel Execution) يعني أن يشغّل مشغّل الاختبارات عدة اختبارات في وقت واحد: في عمّال (workers) منفصلين على جهاز واحد، أو في أجزاء (shards) موزّعة على عدة أجهزة. فحزمة اختبارات لإتمام الشراء تستغرق 24 دقيقة بعامل واحد قد تنتهي في ربع هذا الوقت تقريباً بأربعة عمّال، بشرط ألا يستخدم اختباران الحساب أو السلة أو الملف نفسه في اللحظة نفسها.
العزل هو ما يجعل التوازي آمناً. فقد بيّن درس بيانات الاختبار والتجهيزات والخطافات كيف يُعدّ الاختبار الواحد بياناته وينظّفها؛ أما هذا الدرس فينظر إلى الحزمة كاملة: ما الذي تتشاركه الاختبارات دون قصد، وكيف توزّع المشغّلات الاختبارات على العمّال والأجهزة، وكيف تُصلح الاعتماديات التي يكشفها التشغيل المتوازي.
لماذا يهم
الاختبارات التي يعتمد بعضها على بعض تعطي نتائج لا يثق بها أحد. ينجح اختبار في التشغيل الكامل فقط لأن اختباراً سابقاً أنشأ الطلب الذي يقرؤه، ثم يفشل حين يشغّله أحد وحده أو يضيف عاملاً؛ ويعدّل اختباران المستخدم نفسه في اللحظة نفسها فيفشل أحدهما مرة في كل عشرين تشغيلاً. حالات الفشل هذه تبدو كأنها عيوب في المنتج، وتكلّف ساعات من التحقيق، وتعلّم الفريق إعادة تشغيل البناءات الحمراء بدلاً من قراءتها. ويعدّ Martin Fowler غياب العزل سبباً شائعاً للاختبارات غير الحتمية (non-deterministic).
والسرعة هي النصف الآخر. فالحزمة التي تستغرق ساعة تُشغَّل أقل، وتصل نتائجها بعد أن ينتقل المطور إلى عمل آخر. والتنفيذ المتوازي يُبقي الحزمة المتنامية سريعة بما يكفي لتشغيلها مع كل تغيير، ولا ينجح إلا إذا كانت الاختبارات معزولة. ويساعد العزل تقرير الخطأ أيضاً: فالاختبار الذي يفشل وحده، في كل مرة، ببياناته الخاصة، هو خطوات إعادة إنتاج جاهزة للمطور.
كيف تشغّل المشغّلات الاختبارات بالتوازي
في Playwright Test وJest وpytest-xdist، العامل (worker) عملية مستقلة في نظام التشغيل تشغّل الاختبارات واحداً بعد الآخر. ومع أربعة عمّال تعمل أربعة اختبارات في اللحظة نفسها. ذاكرتها منفصلة، لكن بيئة الاختبار وقاعدة بياناتها والملفات على القرص ومنافذ الشبكة ليست كذلك. وتختلف المشغّلات فيما تقسّمه افتراضياً، وفي طريقة منح كل عامل موارده الخاصة:
- يشغّل Playwright Test ملفات الاختبار بالتوازي، وتعمل الاختبارات داخل الملف الواحد بالترتيب في العامل نفسه، إلا إذا ضبطت
fullyParallel: trueأوtest.describe.configure({ mode: 'parallel' }). وتحدّد العدد بالخيارworkersفي الإعدادات أو بـ--workers. ويحصل كل اختبار على سياق متصفح (browser context) جديد، أي جلسة متصفح معزولة تشبه نافذة تصفح خاص جديدة، بملفات تعريف ارتباط وتخزين خاصة به، وبعد أي فشل يستبدل Playwright العامل بعملية جديدة. - يحتاج pytest إلى الإضافة pytest-xdist: الأمر
pytest -n 4، أو-n autoلعامل واحد لكل معالج. وتعيد التجهيزةworker_idالقيمgw0وgw1وهكذا. - يوزّع Jest ملفات الاختبار على عمليات عمّال، ويشغّل اختبارات الملف الواحد بالترتيب؛ والخيار
--runInBandيشغّل كل شيء تسلسلياً في عملية واحدة. - يشغّل JUnit 5 الاختبارات تسلسلياً في خيط (thread) واحد افتراضياً. والتنفيذ المتوازي اختياري: معامل الإعداد
junit.jupiter.execution.parallel.enabled=trueيفعّله، لكن الاختبارات تظل تعمل واحداً بعد الآخر حتى تجعلها تعمل بالتوازي فعلاً، مثلاً بـjunit.jupiter.execution.parallel.mode.default=concurrent. والاختبارات المتوازية خيوط داخل JVM واحد، لذا تتشارك الحقول الساكنة (static) والكائنات الوحيدة. ويصرّح@ResourceLockبأن الاختبارات تستخدم مورداً مشتركاً يحتاج إلى وصول متزامن. - موارد لكل عامل: حين يتعذّر إنشاء شيء لكل اختبار، أعطِ كل عامل نسخته الخاصة. فقيمة
testInfo.parallelIndexفي Playwright تتراوح من 0 إلى عدد العمّال ناقص 1، فيأخذ العامل 2 الحساب 2 من مجموعة حسابات جُهّزت قبل التشغيل. - استثناءات تسلسلية: الاختبار الذي يغيّر شيئاً عاماً، مثل نسبة ضريبة القيمة المضافة أو علم ميزة على مستوى الموقع، لا يمكن أن يعمل بجانب اختبارات أخرى. انقله إلى تشغيل منفصل صغير بالخيار
--workers=1، وجدوله بحيث لا يتزامن أبداً مع التشغيل الرئيسي على البيئة نفسها، بدلاً من جعل الحزمة كلها تسلسلية.
مثال تطبيقي: ثلاثة اختبارات لا تفشل إلا بالتوازي
حزمة لإتمام الشراء من 120 اختباراً في المتصفح تستغرق 24 دقيقة بعامل واحد. يضبط الفريق fullyParallel: true وأربعة عمّال، فينخفض زمن التشغيل إلى 7 دقائق، لكن ثلاثة اختبارات صارت تفشل في بعض التشغيلات. وما زالت الحزمة كلها تنجح في كل مرة مع --workers=1، لذا فالسبب المرجّح تداخل بين الاختبارات لا عيب في المنتج.
كل الاختبارات تسجّل الدخول بالحساب qa.shared@example.com. يضيف اختبار «إجمالي السلة» منتجين بسعر 150.00 و80.00 ويتوقع 230.00، لكنه يرى أحياناً 0.00، لأن اختبار «تفريغ السلة» الذي يعمل على عامل آخر يفرّغ السلة نفسها. ويتوقع اختبار «سجل الطلبات» أن تعرض القائمة ثلاثة طلبات، هي التي تنشئها اختبارات الطلب الثلاثة في ملفه؛ ولم ينجح إلا لأنه كان يعمل بعدها في العامل نفسه، وهو يفشل حتى حين يُشغَّل وحده. ويحفظ اختبار «تنزيل الفاتورة» الملف في downloads/invoice.pdf، حيث قد تكتب فوقه فاتورة عامل آخر.
حساب مشترك واحد
- العامل 1 اختبار «إجمالي السلة»
- بيئة الاختبار سلة واحدة لكل الاختبارات
- العامل 2 اختبار «تفريغ السلة»
- 1 من العامل 1 إلى بيئة الاختبار: إضافة منتجين بسعر 150.00 و80.00
qa.shared@example.com - 2 من العامل 2 إلى بيئة الاختبار: تفريغ السلة في اللحظة نفسها الحساب نفسه، إذن السلة نفسها
qa.shared@example.com - 3 العامل 2 داخليًا: التأكيد: السلة فارغة نجاح
- 4 من العامل 1 إلى بيئة الاختبار: فتح السلة وقراءة الإجمالي
- 5 من بيئة الاختبار إلى العامل 1: يعرض إجمالياً قدره 0.00 ردّ
- 6 العامل 1 داخليًا: التأكيد: الإجمالي 230.00 فشل
النتيجة: يفشل فقط حين يتزامن الاختباران؛ وكل منهما ينجح وحده ومع عامل واحد. فشل
حساب جديد لكل عامل
- العامل 1 اختبار «إجمالي السلة»
- بيئة الاختبار حساب وسلة منفصلان لكل عامل
- العامل 2 اختبار «تفريغ السلة»
- 1 من العامل 1 إلى بيئة الاختبار: إضافة منتجين بسعر 150.00 و80.00
qa+3f9a…@example.com - 2 من العامل 2 إلى بيئة الاختبار: تفريغ سلته الخاصة في اللحظة نفسها حساب مختلف، إذن سلة مختلفة
qa+c41e…@example.com - 3 العامل 2 داخليًا: التأكيد: السلة فارغة نجاح
- 4 من العامل 1 إلى بيئة الاختبار: فتح السلة وقراءة الإجمالي
- 5 من بيئة الاختبار إلى العامل 1: يعرض إجمالياً قدره 230.00 ردّ
- 6 العامل 1 داخليًا: التأكيد: الإجمالي 230.00 نجاح
النتيجة: التوقيت نفسه وسلتان منفصلتان: ينجح الاختباران مهما تداخلت خطواتهما. نجاح
- ينشئ كل عامل حساباً جديداً خاصاً به في تجهيزة على مستوى العامل، مع UUID في البريد الإلكتروني حتى لا تتصادم الأجزاء أيضاً.
- الاختبارات داخل العامل الواحد ما زالت تتشارك هذا الحساب واحداً بعد الآخر، لذا يفرّغ الخطاف
beforeEachالسلة قبل كل اختبار بدلاً من الوثوق بالاختبار السابق. - ينشئ «سجل الطلبات» طلباته الثلاثة عبر الواجهة البرمجية (API) ويتحقق من معرّفاتها، لا من العدد الإجمالي.
- تُحفظ الفاتورة في
test.info().outputPath()، وهو مجلد فريد لكل اختبار. - يُتأكَّد من الإصلاح بالخيار
--repeat-each=10مع أربعة عمّال، لأن تشغيلاً أخضر واحداً لا يثبت الكثير.
// fixtures.ts: one fresh account per worker process
import { test as base } from '@playwright/test';
import { randomUUID } from 'node:crypto';
type Account = { id: string; email: string };
export const test = base.extend<{}, { account: Account }>({
account: [async ({ playwright }, use) => {
const api = await playwright.request.newContext({ baseURL: process.env.BASE_URL });
const email = `qa+${randomUUID()}@example.com`; // unique in every shard
const res = await api.post('/api/test-accounts', { data: { email } });
const account: Account = await res.json();
await use(account); // shared by this worker's tests
await api.delete(`/api/test-accounts/${account.id}`);
await api.dispose();
}, { scope: 'worker' }],
});
// cart.spec.ts
import { test } from './fixtures';
test.beforeEach(async ({ request, account }) => {
await request.delete(`/api/test-accounts/${account.id}/cart`); // start empty
});
test('download invoice', async ({ page, account }) => {
// ...sign in as account.email and place an order...
const downloading = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download invoice' }).click();
await (await downloading).saveAs(test.info().outputPath('invoice.pdf'));
});أخطاء شائعة
- تفعيل التنفيذ المتوازي دون التحقق أولاً مما تتشاركه الاختبارات، ثم لوم المشغّل على حالات الفشل التي يكشفها.
- إصلاح الاعتماد على الترتيب بفرض ترتيب، عبر الوضع التسلسلي أو ترقيم أسماء الملفات، بدلاً من أن يبني كل اختبار حالته الخاصة.
- استخدام رقم العامل وحده جزءاً فريداً في الاسم، فيستخدم العامل 0 في كل جزء الحساب نفسه.
- التحقق من أعداد إجمالية مثل «القائمة فيها ثلاثة طلبات» في بيئة مشتركة بدلاً من التحقق من السجلات التي أنشأها الاختبار.
- الحكم على تشغيل مقسّم إلى أجزاء بنتيجة جزء واحد بدلاً من التقرير المدمج لكل الأجزاء.
- إعادة تشغيل حالات الفشل الناتجة عن التداخل حتى تنجح، مما يخفي الحالة المشتركة بدلاً من إزالتها.
من منظور ضمان الجودة
العزل شيء يستطيع المختبِر فحصه والمطالبة به. قبل أن تصبح الحزمة متوازية، وكلما راجعت اختباراً آلياً، طبّق أربعة فحوص: هل ينجح الاختبار وحده، وبترتيب عشوائي، ومرتين متتاليتين، ومع عدة عمّال؟ في Jest الخيار --randomize، والإضافة pytest-randomly تخلط ترتيب اختبارات pytest، وفي Playwright الخيار --repeat-each. ثم اسأل: ماذا يغيّر كل اختبار، ومن غيره يرى هذا التغيير؟ كثير من الإصلاحات يحتاج إلى المطورين أو فريق المنصة: واجهة برمجية لإنشاء حسابات اختبار، وطريقة لتفريغ السلة، ومستأجر (tenant) منفصل لكل تشغيل، وبيئة تتحمل حمل أربعة عمّال.
حين يفشل اختبار بالتوازي فقط، سجّل ما يميّز التداخل من عيب في المنتج: العامل والجزء اللذين عمل عليهما، والحساب ومعرّفات السجلات التي استخدمها، وما كان يعمل في الوقت نفسه، وهل ينجح وحده ومع عامل واحد. والتداخل المؤكَّد عيب في حزمة الاختبارات له مالك وإصلاح، لا ضجيج يُعاد تشغيله.
تمرين عملي
اجعل حزمة الإعدادات آمنة للتشغيل المتوازي
الحزمة أدناه تعمل بعامل واحد، ويريد الفريق تشغيلها بأربعة عمّال وجزأين. لكل اختبار، اذكر ما يتشاركه مع الاختبارات الأخرى، وكيف تعزله، وأي الاختبارات يمكن أن يبقى كما هو وأيها يجب أن يعمل منفصلاً. ثم اذكر ما يتغيّر بسبب الجزأين.
account-settings.spec.ts (Playwright)
Today: workers: 1
Planned: workers: 4, fullyParallel: true
Planned CI: --shard=1/2 and --shard=2/2, both against the same staging environment
Every test signs in as qa.settings@example.com
1 change name sets the display name to "Sara A."
2 name in header expects "Sara A." in the page header
3 enable 2FA turns on two-factor sign-in for the account
4 export data saves the export to downloads/export.zip, then opens it
5 default theme sets the site-wide default theme to dark in the admin panel
6 language list reads the list of supported languages and changes nothing- لكل اختبار، اكتب ما يقرؤه وما يغيّره.
- علّم كل تغيير يقرؤه أو يغيّره اختبار آخر أيضاً.
- اختر إصلاحاً لكل منها: بيانات خاصة، أو ملف خاص، أو مورد لكل عامل، أو تشغيل تسلسلي منفصل.
- افحص ما يضيفه الجزآن، علماً بأن كليهما يستخدم بيئة staging نفسها.
إظهار الإجابة النموذجية
الاختباران 1 و2: يعتمد الاختبار 2 على أن يكون الاختبار 1 قد عمل قبله على الحساب نفسه، ومع fullyParallel قد يعمل قبله أو على عامل آخر. الإصلاح: يحصل كل اختبار على حساب جديد من تجهيزة، ويضبط الاختبار 2 الاسم بنفسه عبر الواجهة البرمجية قبل أن يفحص رأس الصفحة.
الاختبار 3: تفعيل الدخول بخطوتين على الحساب المشترك يُفسد تسجيل الدخول لكل الاختبارات الأخرى، ومنها الاختبار 6 الذي يقرأ فقط. يحتاج إلى حساب جديد خاص به يمكنه حذفه بعد ذلك.
الاختبار 4: قد يكتب عاملان في downloads/export.zip في الوقت نفسه. احفظ في test.info().outputPath('export.zip') بدلاً من ذلك.
الاختبار 5: إعداد على مستوى الموقع لا يمكن عزله لكل حساب. انقله إلى تشغيل منفصل بالخيار --workers=1 يعمل مرة واحدة قبل مهام الأجزاء أو بعدها وليس في الوقت نفسه أبداً، لأنها جميعاً تستخدم بيئة staging نفسها. ويضبط هذا الاختبار السمة التي يتوقعها في بدايته ويعيد السمة الافتراضية في التنظيف. أما الاختبار 6 فيقرأ بيانات مرجعية مشتركة فقط، ويمكن أن يبقى كما هو متى سجّل الدخول بحساب خاص به أو بحساب للقراءة فقط.
الأجزاء: يستخدم الجزآن بيئة staging نفسها، لذا يجب أن تكون أسماء الحسابات فريدة عبر الأجزاء، بـUUID مثلاً، لأن رقم العامل وحده يتكرر. ادمج تقريري الجزأين في تقرير واحد قبل الحكم على التشغيل، وتأكد من الإصلاحات بعدة تشغيلات متكررة بأربعة عمّال.
أسئلة شائعة
هل يجعل التنفيذ المتوازي الاختبارات متذبذبة؟
في الغالب يكشف مشكلات كانت موجودة: حالة مشتركة واعتماداً على الترتيب أخفاهما ترتيب ثابت وتوقيت ثابت. لكنه قد يضيف سبباً حقيقياً واحداً هو الحمل: فأربعة عمّال قد يبطئون بيئة اختبار صغيرة حتى تنتهي مهلات الانتظار. إذا اختفت حالات الفشل مع عدد أقل من العمّال ولم يلمس أي اختبار آخر بيانات الاختبار الفاشل، فاشتبه في الحمل لا في الحالة المشتركة.
ما الفرق بين العمّال والأجزاء؟
العمّال عمليات متوازية على جهاز واحد (وخيوط في JUnit 5)؛ والأجزاء أقسام من الحزمة تشغّلها أجهزة أو مهام CI منفصلة. ويمكن الجمع بينهما: أربعة أجزاء بأربعة عمّال لكل منها تشغّل حتى ستة عشر اختباراً في وقت واحد. يتشارك العمّال معالج الجهاز وقرصه، بينما لا تتشارك الأجزاء إلا بيئة الاختبار، ويجب دمج تقاريرها.
هل يجب أن يكون لكل اختبار حساب خاص؟
الحساب الجديد لكل اختبار هو الخيار الأكثر أماناً حين يكون إنشاء الحسابات رخيصاً. والحساب الواحد لكل عامل حلّ وسط شائع: اختبارات العامل الواحد تعمل واحداً بعد الآخر، فينجح ذلك إذا أعاد كل اختبار في بدايته ضبط ما يغيّره. أما البيانات المخصّصة للقراءة فقط، مثل كتالوج لا يعدّله أي اختبار، فيمكن أن تتشاركها كل الاختبارات.
المراجع
- Eradicating Non-Determinism in Tests (Martin Fowler)لماذا تُفقد الاختبارات المتذبذبة الحزمةَ قيمتها، وأسبابها الشائعة (العزل وعدم التزامن والخدمات البعيدة والوقت وتسرّب الموارد)، والحجر كاستجابة.
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- Best Practices — Playwrightاختبر السلوك الذي يراه المستخدم، واجعل الاختبارات معزولة، وفضّل المحدِّدات المتينة والتأكيدات التي تنتظر، واستخدم التتبّع لتصحيح الأخطاء.
- Best Practices — Cypressإرشادات Cypress لاختيار العناصر بسمات مخصّصة، وتجنّب الانتظار الثابت، وإبقاء الاختبارات مستقلة.
تدرّب بأدوات متقن
توليد معرّفات UUID v4 (عشوائية) وv7 (مرتّبة زمنياً) بالجملة، والتحقق من المعرّفات الموجودة.
ولّد عناوين بريد إلكتروني اختبارية بنطاقات محجوزة آمنة (example.com و.test) مع متغيرات العنونة بعلامة الزائد (plus-addressing).
ولّد سجلات مستخدمين وهمية (الاسم واسم المستخدم والبريد الإلكتروني وكلمة المرور وبذرة الصورة الرمزية والدور) لحسابات الاختبار.
مصنع متكامل لبيانات الاختبار الاصطناعية: مستخدمون وعملاء وشركات وعناوين وأرقام وتواريخ وسلاسل نصية بصيغة JSON أو CSV أو SQL أو XML.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.