إذا كانت اختبارات البريد الإلكتروني الخاصة بك تعمل بشكل مثالي على جهازك المحمول وتنهار بمجرد وصولها إلى بيئة التكامل المستمر (CI)، فلست وحدك. الاستجابة المعتادة هي توزيع استدعاءات sleep عبر كود الاختبار أو زيادة عدد المحاولات (retry count) حتى ينجح البناء (build). قد يؤدي ذلك إلى تهدئة الضجيج ليوم واحد، لكنه لا يصلح الخطأ، بل يخفيه فحسب.
المشكلة الحقيقية تكمن في كيفية تحديد الاختبار للبريد الإلكتروني الذي يجب فتحه.
مشكلة صندوق الوارد المشترك
على جهازك المحلي، تقوم بتشغيل اختبار واحد في كل مرة. يصل بريد إلكتروني واحد. تأخذه. الأمر بسيط.
أما بيئة التكامل المستمر (CI) فهي بيئة مختلفة تمامًا. قد يؤدي طلب سحب (pull request) واحد إلى تشغيل أربعة أو ثمانية أو ستة عشر وظيفة متوازية (parallel jobs). إذا كانت جميعها تشترك في صندوق وارد للاختبار — سواء كان خادم Mailosaur، أو صندوق Mailtrap، أو حسابًا حقيقيًا على نطاق تجريبي (staging domain) — فإنها جميعًا تكتب في نفس الوعاء (bucket) في نفس الوقت. الوظيفة (أ) ترسل إعادة تعيين كلمة المرور. الوظيفة (ب) ترسل دعوة. الوظيفة (ج) تعيد محاولة تدفق ترحيبي فاشل. وفي هذه الأثناء، تضيف برامج الخلفية (background workers) وطوابير التسليم تذبذبًا (jitter) لا يمكنك التحكم فيه.
عندما تحاول كل وظيفة الوصول إلى صندوق الوارد المشترك هذا وتطلب أحدث رسالة بعنوان "Reset your password"، يتحول الأمر إلى سباق. الاختبار الذي يفوز يحصل على البريد الإلكتروني الصحيح. أما الاختبار الذي يخسر، فينقر على رابط مخصص لوظيفة أخرى، ويجري عمليات تحقق (assertions) مقابل محتوى خاطئ، ويفشل بخطأ يبدو وكأنه مشكلة توقيت. إنها ليست مشكلة توقيت، بل هي مشكلة هوية.
لماذا يفشل نمط "أحدث رسالة"
من السهل الوقوع في هذا النمط الهش لأنه يبدو بديهيًا:
- تشغيل تدفق المستخدم (user flow).
- فحص صندوق الوارد كل بضع ثوانٍ.
- فتح أحدث رسالة تطابق سطر الموضوع.
- النقر على الرابط الأول وإجراء عمليات التحقق (assertions).
ينهار هذا النمط لعدة أسباب تتجاوز مجرد التوازي البسيط. فقد تصل محاولة إعادة (retry) من تشغيل فاشل سابق متأخرة، لتصبح فجأة هي أحدث رسالة في اللحظة التي يقوم فيها اختبارك الحالي بالفحص. قد تقوم برامج الخلفية داخل تطبيقك بوضع رسالتين في الطابور وتسليم الثانية قبل الأولى. كما أن أسطر الموضوع وحدها تعد معرفات ضعيفة؛ فقد يرسل تطبيقك التجريبي رسائل مماثلة من مسارات مختلفة. أما الترتيب حسب الطابع الزمني (timestamp) فهو أسوأ مما يبدو، لأن تفاوت التوقيت (clock skew) بين مشغل الـ CI ومزود البريد أمر واقعي، وغالبًا ما تقوم واجهات برمجة تطبيقات البريد (mail APIs) بتخزين الفهارس مؤقتًا أو تجميعها.
تصبح الطوابع الزمنية غير دقيقة في البيئات المزدحمة. أنت بحاجة إلى شيء مباشر.
ما هو رمز التشغيل (Run Token) في الواقع
رمز التشغيل ليس سوى سلسلة فريدة يتم إنشاؤها في بداية اختبارك وحقنها في البريد الإلكتروني الذي يرسله تطبيقك. لا يحتاج أن يكون ظاهرًا للمستخدم، ولا يحتاج أن يكون أنيقًا. كل ما يحتاجه هو ضمان قدرتك على إثبات أن هذه الرسالة المحددة تنتمي إلى تنفيذ هذا الاختبار المحدد.
الأمثلة الملموسة هي الأفضل. قبل بدء الاختبار، قم بإنشاء رمز مثل:
- معرف UUID:
550e8400-e29b-41d4-a716-446655440001 - معرف طلب مرتبط بالبناء (build-scoped request ID):
req_ci_build_4821_a7f3 - رمز دعوة (invite slug) أو لاحقة بيانات وصفية (metadata suffix):
signup-token-8k2m9n - سلسلة سداسية عشرية (hex string) عشوائية ينشئها مشغل الاختبار:
test-run-a4f9c2d1
إذا كنت تتحكم في كود الخلفية (backend)، فقم بتمرير الرمز إلى سياق البريد الإلكتروني وقم بعرضه في مكان ما في نص الرسالة. أما إذا كنت تختبر تطبيقًا بنظام الصندوق الأسود (black-box application)، فابحث عما إذا كان التطبيق يقبل بالفعل حقل مرجع يمكنك استغلاله. وإذا لم يكن الأمر كذلك، يمكنك أحيانًا تضمين الرمز في الجزء المحلي للمستلم باستخدام "plus addressing" — testuser+a4f9c2d1@example.com — رغم أن هذا يعمل فقط إذا كان تطبيقك يحافظ عليه ويعيد إرساله في البريد الإلكتروني.
النقطة هي التوقف عن المطابقة بناءً على البيانات الوصفية (metadata) التي يمتلكها نظام البريد بالفعل. طابق بناءً على البيانات التي يمتلكها اختبارك.
النمط الموثوق
استبدل خوارزمية "أحدث رسالة" بعملية بحث ضيقة تعتمد على الرمز:
- قم بإنشاء رمز التشغيل قبل تشغيل أي تدفق.
- ابدأ إجراء المستخدم، مع التأكد من أن التطبيق سيضمن الرمز في البريد الإلكتروني الصادر.
- افحص مزود البريد باستخدام عوامل تصفية (filters) تقتصر على ذلك الرمز. إذا كانت واجهة برمجة التطبيقات (API) تدعم البحث في نص الرسالة، فاستخدمها. وإذا لم تكن كذلك، فقم بجلب الرسائل المرشحة واستخدم
grepللبحث في نصوصها من جانب العميل. - تحقق من وجود الرمز في نص الرسالة قبل لمس أي روابط أو أزرار أو رموز تحقق.
- عندها فقط، استخرج رابط التأكيد أو الرمز وتابع العملية.
هذا التسلسل مهم للغاية. إذا استخرجت الرابط أولاً ثم تحققت من الرمز ثانياً، فستكون قد نقرت بالفعل على البريد الإلكتروني الخطأ. عملية التحقق (assertion) هي حارس البوابة الخاص بك.
من الناحية العملية، يجب أن يبحث المساعد الخاص بك عن Subject:"Welcome to AppName" AND Body:"a4f9c2d1" بدلاً من Subject:"Welcome to AppName" sort:-received. توفر العديد من خدمات اختبار البريد الإلكتروني واجهات برمجة تطبيقات (APIs) للبحث تقبل فلاتر محتوى نص الرسالة (body). استخدمها. إذا كنت تعمل مع مزود أبسط، فاجعل منطق الاستطلاع (polling logic) في مكان واحد حتى تتمكن من إضافة تصفية من جانب العميل (client-side filtering) بشكل متسق عبر كل اختبار.
ثلاث قواعد للحفاظ على دقة النظام
يضمن رمز التشغيل (run token) دقة الاختيار، ولكنك لا تزال بحاجة إلى الانضباط في كيفية إجراء الاستطلاع وما تفعله عندما تسوء الأمور.
سجل حالة صندوق الوارد عند الفشل. عندما يفشل اختبار ما، قم بإخراج معرف صندوق الوارد، وسطر الموضوع الذي استعلمت عنه، والنافذة الزمنية الدقيقة، وعدد الرسائل التي طابقت معاييرك. هذا يحول خطأ "لم يتم العثور على البريد الإلكتروني" الغامض إلى قصة ملموسة. إذا التقطت المهمة 7823 رسالة إعادة محاولة من المهمة 7821 لأنها وصلت بعد ثلاث ثوانٍ، فيجب أن تجعل سجلاتك ذلك واضحاً. بدون هذا السياق، ستلوم التوقيت وتضيف أمر sleep آخر.
اجعل كل عمليات استطلاع البريد الإلكتروني في ملف مساعد واحد. لا تشتت استدعاءات setTimeout و cy.task عبر عشرين ملف اختبار. قم بمركزة المنطق الذي ينتظر الرسائل، ويعيد محاولة استدعاء الـ API، ويطبق التراجع التدريجي (backoff). إذا استخدم كل اختبار نفس المساعد، فستظل قواعد التصفية الخاصة بك متسقة، وعندما تقوم بتحسين منطق البحث، ستستفيد كل الاختبارات. كما يسهل ذلك فرض التحقق من الرمز؛ فإذا كان المساعد يتطلب وسيط رمز (token argument)، فلن يتمكن أحد من العودة عن طريق الخطأ إلى وسيلة "أحدث رسالة" المساعدة.
راقب عمليات إعادة المحاولة. عمليات إعادة محاولة الاختبار شائعة في التكامل المستمر (CI)، ولكن كل إعادة محاولة تنشئ بريداً إلكترونياً آخر في صندوق الوارد. إذا نجح اختبارك في المحاولة الثالثة، فقد تحتفل وتمضي قدماً. ما يفوتك هو أن المحاولتين الأولى والثانية كشفتا عن خطأ حقيقي — حالة سباق (race condition)، أو إرسال مكرر، أو فهرس مفقود — قامت الرسائل الإضافية بإخفائه. إذا كان لابد من استخدام إعادة المحاولة، فتحقق مما إذا كان صندوق الوارد يحتوي على مكررات غير متوقعة بعد الفشل. والأفضل من ذلك، فكر في تنظيف صندوق الوارد أو استخدام عنوان فريد لكل مهمة إذا كان مزود الخدمة يدعم صناديق الوارد الديناميكية. لا ينبغي أن تصبح إعادة المحاولة استراتيجية لاستيعاب منطق اختيار غير موثوق.
الخلاصة الحقيقية
إن فرز صندوق الوارد حسب التاريخ وأخذ النتيجة الأولى ليس اختباراً؛ بل هو تخمين مغلف بالكود. لا يكلف رمز التشغيل (run token) شيئاً تقريباً — متغير نصي واحد، معلمة تصفية إضافية واحدة، وربما تغيير بسيط في القالب — وهو يمنح اختبارك هوية حتمية (deterministic identity). فهو يثبت أن الرسالة التي أمامك تنتمي إلى عملية التشغيل التي تنفذها الآن.
توقف عن إضافة أوامر sleep والأمل في أن تعمل الشبكة بشكل جيد. قم بإنشاء رمز، وضعه في البريد الإلكتروني، وابحث عنه مباشرة. ستكون عمليات الـ CI الخاصة بك أسرع، وستكون سجلاتك قابلة للقراءة، وستثق أخيراً فيما تخبرك به مجموعة اختبارات البريد الإلكتروني.
