अगर आपके ईमेल टेस्ट आपके लैपटॉप पर बिल्कुल सही चलते हैं और CI पर पहुँचते ही फेल हो जाते हैं, तो आप अकेले नहीं हैं। आमतौर पर लोग टेस्ट कोड में sleep कॉल डाल देते हैं या बिल्ड पास होने तक रिट्राय (retry) काउंट बढ़ा देते हैं। यह एक दिन के लिए शोर को शांत कर सकता है, लेकिन यह बग को ठीक नहीं करता। यह केवल उसे छिपाता है।
असली समस्या यह है कि आपका टेस्ट यह कैसे पहचानता है कि कौन सा ईमेल खोलना है।
शेयर्ड इनबॉक्स की समस्या
अपनी लोकल मशीन पर, आप एक बार में एक ही टेस्ट चलाते हैं। एक ईमेल आता है। आप उसे ले लेते हैं। सरल है।
CI पूरी तरह से एक अलग वातावरण है। एक सिंगल पुल रिक्वेस्ट (pull request) चार, आठ या सोलह समानांतर (parallel) जॉब्स ट्रिगर कर सकती है। यदि वे सभी एक ही शेयर्ड इनबॉक्स साझा करते हैं—चाहे वह Mailosaur सर्वर हो, Mailtrap इनबॉक्स हो, या स्टेजिंग डोमेन पर कोई वास्तविक अकाउंट हो—तो वे सभी एक ही समय में एक ही बकेट में लिख रहे होते हैं। जॉब A पासवर्ड रीसेट भेजता है। जॉब B एक इनवाइट भेजता है। जॉब C एक विफल वेलकम फ्लो को रिट्राय करता है। इस बीच, बैकग्राउंड वर्कर्स और डिलीवरी क्यूज़ (queues) ऐसा जिटर (jitter) पैदा करते हैं जिसे आप नियंत्रित नहीं कर सकते।
जब हर जॉब उस शेयर्ड इनबॉक्स में जाकर "Reset your password" विषय वाले सबसे नए मैसेज की मांग करती है, तो यह एक रेस (race) बन जाती है। जो टेस्ट जीतता है उसे सही ईमेल मिलता है। जो टेस्ट हार जाता है वह किसी दूसरे जॉब के लिए बने लिंक पर क्लिक करता है, गलत कंटेंट के खिलाफ एसेर्शन (assertion) करता है, और एक ऐसे एरर के साथ फेल हो जाता है जो टाइमिंग की समस्या जैसा दिखता है। यह टाइमिंग की समस्या नहीं है। यह पहचान (identity) की समस्या है।
"Newest Message" क्यों विफल होता है
इस नाजुक पैटर्न में फंसना आसान है क्योंकि यह सहज लगता है:
- यूजर फ्लो ट्रिगर करें।
- हर कुछ सेकंड में इनबॉक्स को पोल (poll) करें।
- विषय (subject line) से मेल खाने वाले सबसे हालिया मैसेज को खोलें।
- पहले लिंक पर क्लिक करें और एसेर्शन चलाएं।
यह केवल पैरेललिज्म के अलावा कई अन्य कारणों से विफल हो जाता है। पिछले विफल रन का एक रिट्राय देर से आ सकता है, और ठीक उसी समय सबसे नया मैसेज बन सकता है जब आपका वर्तमान टेस्ट पोल कर रहा हो। आपके एप्लिकेशन के अंदर बैकग्राउंड वर्कर्स दो ईमेल को क्यू में रख सकते हैं और पहले वाले से पहले दूसरे को डिलीवर कर सकते हैं। सिर्फ सब्जेक्ट लाइन एक कमजोर पहचानकर्ता है; आपका स्टेजिंग एप्लिकेशन अलग-अलग रास्तों से समान ईमेल भेज सकता है। टाइमस्टैम्प के आधार पर सॉर्ट करना जितना दिखता है उससे कहीं अधिक खराब है क्योंकि CI रनर और मेल प्रोवाइडर के बीच क्लॉक स्क्यू (clock skew) वास्तविक होता है, और मेल API अक्सर अपने इंडेक्स को कैश या बैच करते हैं।
व्यस्त वातावरण में टाइमस्टैम्प धुंधले हो जाते हैं। आपको कुछ सीधा चाहिए।
रन टोकन (Run Token) वास्तव में क्या है
रन टोकन आपके टेस्ट की शुरुआत में जेनरेट किया गया एक यूनिक स्ट्रिंग है जिसे आपके एप्लिकेशन द्वारा भेजे गए ईमेल में डाल दिया जाता है। इसे यूजर को दिखने की जरूरत नहीं है, और इसे सुंदर दिखने की भी जरूरत नहीं है। इसे केवल यह गारंटी देने की आवश्यकता है कि आप साबित कर सकें कि यह विशिष्ट मैसेज इसी विशिष्ट टेस्ट निष्पादन (execution) का है।
ठोस उदाहरण सबसे अच्छा काम करते हैं। टेस्ट शुरू होने से पहले, इस तरह का एक टोकन जेनरेट करें:
- एक UUID:
550e8400-e29b-41d4-a716-446655440001 - एक बिल्ड-स्कोपेड रिक्वेस्ट आईडी:
req_ci_build_4821_a7f3 - एक इनवाइट स्लग या मेटाडेटा सफिक्स:
signup-token-8k2m9n - टेस्ट रनर द्वारा जेनरेट की गई एक रैंडम हेक्स स्ट्रिंग:
test-run-a4f9c2d1
यदि आप बैकएंड कोड को नियंत्रित करते हैं, तो टोकन को ईमेल कॉन्टेक्स्ट में पास करें और उसे बॉडी में कहीं रेंडर करें। यदि आप एक ब्लैक-बॉक्स एप्लिकेशन का टेस्ट कर रहे हैं, तो देखें कि क्या ऐप पहले से ही कोई ऐसा रेफरेंस फील्ड स्वीकार करता है जिसे आप हाइजैक (hijack) कर सकें। यदि नहीं, तो आप कभी-कभी प्लस एड्रेसिंग (plus addressing) का उपयोग करके प्राप्तकर्ता के लोकल-पार्ट में टोकन को एम्बेड कर सकते हैं—testuser+a4f9c2d1@example.com—हालांकि यह तभी काम करता है जब आपका एप्लिकेशन इसे सुरक्षित रखता है और ईमेल में वापस दिखाता है।
बिंदु यह है कि उस मेटाडेटा पर मैच करना बंद करें जो मेल सिस्टम के पास पहले से है। उस डेटा पर मैच करें जो आपका टेस्ट नियंत्रित करता है।
भरोसेमंद पैटर्न
"Newest message" एल्गोरिदम को एक संकीर्ण, टोकन-संचालित खोज (token-driven search) से बदलें:
- किसी भी फ्लो को ट्रिगर करने से पहले रन टोकन जेनरेट करें।
- यूजर एक्शन शुरू करें, यह सुनिश्चित करते हुए कि एप्लिकेशन आउटबाउंड ईमेल में टोकन शामिल करेगा।
- उस टोकन तक सीमित फिल्टर के साथ मेल प्रोवाइडर को पोल करें। यदि API बॉडी सर्च का समर्थन करता है, तो उसका उपयोग करें। यदि नहीं, तो संभावित मैसेज प्राप्त करें और क्लाइंट-साइड पर उनके बॉडी को
grepकरें। - किसी भी लिंक, बटन या वेरिफिकेशन कोड को छूने से पहले यह एसेर्ट (assert) करें कि मैसेज बॉडी में टोकन मौजूद है।
- उसके बाद ही कन्फर्मेशन URL या कोड निकालें और आगे बढ़ें।
यह क्रम महत्वपूर्ण है। यदि आप पहले लिंक निकालते हैं और बाद में टोकन चेक करते हैं, तो आप पहले ही गलत ईमेल पर क्लिक कर चुके हैं। एसेर्शन आपका गेटकीपर है।
व्यावहारिक रूप से, आपके हेल्पर को Subject:"Welcome to AppName" sort:-received के बजाय Subject:"Welcome to AppName" AND Body:"a4f9c2d1" खोजना चाहिए। कई मेल टेस्टिंग सेवाएँ ऐसे सर्च API प्रदान करती हैं जो बॉडी कंटेंट फ़िल्टर को स्वीकार करते हैं। उनका उपयोग करें। यदि आप किसी सरल प्रोवाइडर के साथ काम कर रहे हैं, तो अपने पोलिंग लॉजिक को एक ही स्थान पर रखें ताकि आप हर टेस्ट में समान रूप से क्लाइंट-साइड फ़िल्टरिंग जोड़ सकें।
सिस्टम को सटीक बनाए रखने के तीन नियम
एक रन टोकन चयन को निश्चित कर देता है, लेकिन आपको इस बात पर अनुशासन बनाए रखने की आवश्यकता है कि आप कैसे पोल करते हैं और जब चीजें गलत होती हैं तो आप क्या करते हैं।
विफलता होने पर इनबॉक्स की स्थिति को लॉग करें। जब कोई टेस्ट फेल हो जाए, तो इनबॉक्स आइडेंटिफायर, आपके द्वारा क्वेरी की गई सब्जेक्ट लाइन, सटीक टाइमस्टैम्प विंडो, और आपके मानदंडों से कितने संदेश मेल खाते हैं, यह आउटपुट के रूप में दिखाएं। यह एक अस्पष्ट "email not found" त्रुटि को एक ठोस जानकारी में बदल देता है। यदि जॉब 7823 ने जॉब 7821 का रिट्राय मैसेज इसलिए उठा लिया क्योंकि वह तीन सेकंड बाद आया था, तो आपके लॉग्स में यह स्पष्ट होना चाहिए। इस संदर्भ के बिना, आप टाइमिंग को दोष देंगे और एक और sleep जोड़ देंगे।
सभी ईमेल पोलिंग को एक ही हेल्पर फ़ाइल में रखें। setTimeout और cy.task कॉल्स को बीस अलग-अलग टेस्ट फ़ाइलों में न फैलाएं। उस लॉजिक को केंद्रीकृत करें जो संदेशों का इंतज़ार करता है, API कॉल को रिट्राय करता है, और बैकऑफ़ लागू करता है। यदि प्रत्येक टेस्ट एक ही हेल्पर का उपयोग करता है, तो आपके फ़िल्टरिंग नियम सुसंगत रहेंगे, और जब आप सर्च लॉजिक में सुधार करेंगे, तो हर टेस्ट को इसका लाभ मिलेगा। यह टोकन चेक लागू करना भी आसान बनाता है; यदि हेल्पर को टोकन आर्गुमेंट की आवश्यकता है, तो कोई भी गलती से "लेटेस्ट मैसेज" के सहारे पर निर्भर नहीं रह पाएगा।
अपने रिट्राइज़ पर नज़र रखें। CI में टेस्ट रिट्राइज़ सामान्य हैं, लेकिन प्रत्येक रिट्राय इनबॉक्स में एक और ईमेल बनाता है। यदि आपका टेस्ट तीसरे प्रयास में पास हो जाता है, तो आप जश्न मना सकते हैं और आगे बढ़ सकते हैं। आप जो मिस कर जाते हैं वह यह है कि पहले और दूसरे प्रयासों ने एक वास्तविक बग को उजागर किया था—जैसे कि रेस कंडीशन, डुप्लिकेट सेंड, या मिसिंग इंडेक्स—जिसे अतिरिक्त संदेशों ने छिपा दिया था। यदि आपको रिट्राइज़ का उपयोग करना ही है, तो जाँचें कि विफलता के बाद इनबॉक्स में अनपेक्षित डुप्लिकेट तो नहीं हैं। इससे भी बेहतर होगा कि आप इनबॉक्स को साफ़ करने या प्रति जॉब एक अद्वितीय एड्रेस का उपयोग करने पर विचार करें, यदि आपका प्रोवाइडर डायनेमिक इनबॉक्स का समर्थन करता है। रिट्राइज़ को अविश्वसनीय सिलेक्शन लॉजिक को छिपाने की रणनीति नहीं बनना चाहिए।
मुख्य निष्कर्ष
इनबॉक्स को तारीख के अनुसार सॉर्ट करना और सबसे ऊपर वाला परिणाम लेना टेस्टिंग नहीं है। यह कोड के रूप में सजाया गया एक अनुमान है। एक रन टोकन की लागत लगभग कुछ भी नहीं है—एक स्ट्रिंग वेरिएबल, एक अतिरिक्त फ़िल्टर पैरामीटर, शायद एक छोटा टेम्पलेट बदलाव—और यह आपके टेस्ट को एक निश्चित पहचान देता है। यह साबित करता है कि आपके सामने वाला संदेश उसी रन का है जिसे आप अभी चला रहे हैं।
sleeps जोड़ना और नेटवर्क के ठीक चलने की उम्मीद करना बंद करें। एक टोकन जेनरेट करें, उसे ईमेल में डालें, और सीधे उसे खोजें। आपके CI रन तेज़ होंगे, आपके लॉग पढ़ने योग्य होंगे, और अंततः आप उस पर भरोसा कर पाएंगे जो आपका ईमेल सूट आपको बता रहा है।
