इसकी शुरुआत एक Slack मैसेज से होती है। बिल्ड रेड (red) है। आप फेलियर (failures) को स्क्रॉल करते हैं, माथे पर बल डालते हैं, और अपने लैपटॉप पर वही टेस्ट फिर से चलाते हैं। ग्रीन (green)। आप CI जॉब को फिर से ट्राई करते हैं। शायद यह कोई छोटी सी चूक थी। लेकिन फेलियर वापस आ जाता है, सर्वर पर जिद्दी और बार-बार होने वाला, लेकिन आपके लिए अदृश्य।
एक ब्राउज़र टेस्ट जो CI में फेल हो जाता है लेकिन लोकल में पास हो जाता है, वह केवल एक परेशानी नहीं है। यह अविश्वास पैदा करता है। टीमें टाइमिंग (timing) को दोष देने लगती हैं। वे ऐसे अस्थायी समाधान (temporary fixes) लागू कर देते हैं जो कभी हटते नहीं। यहाँ एक setTimeout, वहाँ एक .wait(5000)। टेस्ट सुइट धीमा हो जाता है। फेलियर बार-बार आते रहते हैं। वे flaky टेस्ट स्थायी समस्या बन जाते हैं, और अंततः हर कोई रेड पाइपलाइन को बैकग्राउंड शोर (background noise) की तरह मानने लगता है।
यह खतरनाक है। आप ऐसा टेस्ट सुइट नहीं चाहेंगे जो बार-बार झूठी चेतावनी दे।
CI टूटा नहीं है; यह बस अलग है
CI एनवायरनमेंट (environments) रैंडम नहीं होते। वे डिटरमिनिस्टिक (deterministic) होते हैं। समस्या यह है कि वे एक ऐसे सिस्टम के बारे में डिटरमिनिस्टिक होते हैं जो आपका MacBook या आपका Linux वर्कस्टेशन नहीं है। आपका लोकल सेटअप उन अंतरों को छिपा देता है जिन्हें एक क्लीन CI रनर तुरंत उजागर कर देता है।
सोचिए कि कितने हिस्से एक-दूसरे से अलग हो जाते हैं। आपकी लोकल मशीन हॉट मॉड्यूल रीलोडिंग (hot module reloading) के साथ एक डेवलपमेंट सर्वर चला सकती है, जबकि CI ट्री शेकिंग (tree shaking) और मिनिफिकेशन (minification) के साथ एक प्रोडक्शन आर्टिफैक्ट (production artifact) बनाता है। केवल यही कोड पाथ को हटा सकता है या निष्पादन क्रम (execution order) को बदल सकता है। डिपेंडेंसी ट्री (dependency trees) बदल जाते हैं। एक लॉकफ़ाइल (lockfile) जो बिल्कुल एक जैसी दिखती है, वह अलग तरह से रिज़ॉल्व हो सकती है यदि पैकेज मैनेजर का वर्ज़न एक मामूली रिलीज़ से भी अलग हो। नेटवर्क सीक्वेंस बदल जाते हैं। आपका ऑफिस वाई-फाई एक ही स्टेप में स्टेजिंग API को रिज़ॉल्व कर सकता है; CI रनर एक लोड बैलेंसर के पीछे एक अलग क्लस्टर पर जा सकता है, जिससे ऐसी लेटेंसी (latency) पैदा हो सकती है जिसे आप कभी नहीं देख पाते।
ब्राउज़र खुद भी अलग-अलग एनवायरनमेंट में अलग तरह से व्यवहार करते हैं। आपका लोकल Chrome एक्सटेंशन, कैश किए गए क्रेडेंशियल्स (cached credentials), परसिस्टेंट लोकल स्टोरेज और हार्डवेयर एक्सेलेरेशन वाले GPU के साथ आता है। CI हर रन पर एक ब्लैंक प्रोफाइल से शुरू होता है। ब्राउज़र लाइफसाइकिल अलग हो जाते हैं। रेंडरिंग पाथ (rendering paths) अलग हो जाते हैं। जो फ़ॉन्ट्स आपके सिस्टम पर मौजूद हैं, उन्हें CI में बदल दिया जाता है। व्यूपोर्ट साइजिंग (viewport sizing) और डिवाइस पिक्सेल रेशियो अलग-अलग होते हैं, जो रिस्पॉन्सिव ब्रेकपॉइंट्स (responsive breakpoints) को बदल सकते हैं या लेज़ी-लोडिंग (lazy-loading) व्यवहार को प्रभावित कर सकते हैं।
ये कमियाँ वास्तविक हैं। ये मैकेनिकल हैं। इन्हें रैंडम मानकर अनदेखा करने से ये खत्म नहीं हो जातीं।
प्रिव्यू एनवायरनमेंट (Preview Environments) झूठ बोलते हैं
प्रिव्यू एनवायरनमेंट समस्या को और बढ़ा देते हैं। वे मानवीय समीक्षा (human review) के लिए उपयोगी हैं, लेकिन वे प्रोडक्शन नहीं हैं। वे अक्सर असली API होस्ट के बजाय api-staging की ओर इशारा करते हैं। फीचर फ्लैग्स (Feature flags) हर प्रयोग के लिए true हो जाते हैं, जिससे वह कंडीशनल लॉजिक छिप जाता है जिसे प्रोडक्शन में चलाया जाता है। ऑथेंटिकेशन (Authentication) एक स्टेप छोड़ सकता है या एक मॉक टोकन (mock token) इंजेक्ट कर सकता है। कुकीज़ (Cookies) में ढीली नीतियां (relaxed policies) हो सकती हैं। डेटासेट केवल एक छोटा हिस्सा हो सकता है, दस हज़ार के बजाय केवल दस पंक्तियाँ, जिसका अर्थ है कि पेजिनेशन (pagination), सर्च रैंकिंग, या वर्चुअलाइजेशन लॉजिक का कभी परीक्षण ही नहीं हो पाता।
यदि आपका टेस्ट प्रिव्यू URL पर पास हो जाता है लेकिन प्रोडक्शन में फेल हो जाता है, या इसके विपरीत, तो टेस्ट में बग नहीं है। एनवायरनमेंट में बग है।
अंदाज़ा लगाने से पहले लॉग (Log) करें
जब कोई फेलियर पहली बार दिखाई दे, तो टेस्ट में बदलाव करने और उम्मीद करने की प्रवृत्ति से बचें। अंदाज़ा लगाना बंद करें। आपको कॉन्टेक्स्ट (context) को फ्रीज़ करने की ज़रूरत है ताकि आप एक सफल रन की तुलना एक असफल रन से कर सकें।
स्पष्ट संदिग्धों को लॉग करें। फेलियर के समय पेज URL, बिल्ड ID और कमिट SHA (commit SHA) को रिकॉर्ड करें। सक्रिय फीचर फ्लैग्स को नोट करें। API होस्ट, ब्राउज़र का सटीक वर्ज़न और व्यूपोर्ट साइज को कैप्चर करें। ये विवरण एक रहस्यमयी फेलियर को एक पुनरुत्पादित (reproducible) स्थिति में बदल देते हैं।
केवल स्क्रीनशॉट पर भरोसा न करें। दो पेज बिल्कुल एक जैसे दिख सकते हैं जबकि वे पूरी तरह से अलग JavaScript चला रहे हों। एक स्क्रीनशॉट आपको यह नहीं बताएगा कि CI बंडल में एक अतिरिक्त पॉलीफिल (polyfill) शामिल था या लोकल बंडल ने एक चंक (chunk) को छोड़ दिया क्योंकि वह पहले से ही आपके ब्राउज़र कैश में था।
यह भी याद रखें कि DevTools खोलने से टाइमिंग बदल जाती है। DevTools गारबेज कलेक्शन (garbage collection) को टाल सकता है, नेटवर्क प्रायोरिटी (network prioritization) को बदल सकता है, और कुछ रेंडरिंग ऑप्टिमाइज़ेशन को अक्षम कर सकता है। एक टेस्ट जो DOM का निरीक्षण करते समय पास हो जाता है, वह पैनल बंद करते ही और हेडलेस (headless) मोड में चलाते ही फेल हो सकता है। डिबगर (debugger) एक उपयोगी टूल है, लेकिन यह एक तटस्थ पर्यवेक्षक (neutral observer) नहीं है।
क्राइम सीन (Crime Scene) को फिर से बनाएं
यदि आप फेलियर को सटीक रूप से फिर से बनाना चाहते हैं, तो आप केवल अपने लोकल डेवलपमेंट सर्वर को चलाकर बेहतर की उम्मीद नहीं कर सकते। आपको CI की सटीक स्थितियों को दोहराने की आवश्यकता है।
CI ने जो सटीक आर्टिफैक्ट (artifact) बनाया है, उसे ही बनाएं। यदि आवश्यक हो तो उसे डाउनलोड करें। उस आर्टिफैक्ट को एक साधारण स्टैटिक फ़ाइल सर्वर के साथ स्थानीय रूप से सर्व करें, न कि Vite या Webpack डेव मिडलवेयर के साथ। उन्हीं एनवायरनमेंट वेरिएबल्स का उपयोग करें जो CI ने इंजेक्ट किए थे। ब्राउज़र वर्शन से सटीक रूप से मेल खाएं। इसे उसी मोड में चलाएं, headed या headless, क्योंकि focus events, media queries, और autoplay policies अभी भी दोनों के बीच सूक्ष्म तरीकों से भिन्न होते हैं। यदि आपका CI एक Docker कंटेनर का उपयोग करता है, तो स्थानीय रूप से उसी इमेज को चलाएं। अपने व्यक्तिगत ब्राउज़र प्रोफाइल को पूरी तरह से हटा दें।
जब स्थानीय पुनरुत्पादन (local reproduction) अंततः विफल हो जाता है, तब आपके पास एक वास्तविक डिबगिंग सत्र होता है। तब तक, आप केवल परछाइयों का पीछा कर रहे हैं।
सोना बंद करें, इंतज़ार करना शुरू करें
एक flaky ब्राउज़र टेस्ट का सबसे आम जवाब देरी (delay) जोड़ना है। पाँच सेकंड प्रतीक्षा करें। दस सेकंड प्रतीक्षा करें। यह कोई समाधान नहीं है। यह आत्मसमर्पण है। मनमाने विलंब (arbitrary delays) आपके टेस्ट सुइट को धीमा कर देते हैं, झूठा आत्मविश्वास पैदा करते हैं, और नेटवर्क में रुकावट आने पर लोड के तहत फिर भी विफल हो जाते हैं।
इसके बजाय, स्थिति (state) के प्रमाण का इंतज़ार करें। यदि फॉर्म सबमिशन के बाद एक नोटिफिकेशन दिखना चाहिए, तो समय बीतने का इंतज़ार न करें। DOM में एक विशिष्ट नोटिफिकेशन ID के होने का इंतज़ार करें। यदि कोई काउंटर बढ़ना चाहिए, तो टेक्स्ट के मान बदलने का इंतज़ार करें। यदि कोई लोडिंग स्टेट इंटरैक्शन को रोकता है, तो लोडिंग मार्कर के गायब होने का इंतज़ार करें। यदि आप WebSocket या server-sent events के साथ काम कर रहे हैं, तो नेटवर्क स्ट्रीम द्वारा एक विशिष्ट इवेंट उत्पन्न करने का इंतज़ार करें।
Explicit waits आपके टेस्ट को अनुमान लगाने के खेल से बदलकर एक अनुबंध (contract) में बदल देते हैं। टेस्ट कहता है: "मैं तभी आगे बढ़ूँगा जब एप्लिकेशन पुष्टि कर दे कि वह तैयार है।" यह कहने से कहीं अधिक मजबूत है कि, "मैं तभी आगे बढ़ूँगा जब पर्याप्त सेकंड बीत जाएंगे।"
Hydration और गायब होता बटन
आधुनिक React एप्लिकेशन में, hydration विफलताओं के एक विशिष्ट वर्ग का कारण बनता है जिसे स्थानीय डेव सर्वर अक्सर छिपा देते हैं। सर्वर HTML भेजता है। React ब्राउज़र में बूट होता है और इवेंट लिसनर्स (event listeners) जोड़ता है। उस दौरान, आपका टेस्ट किसी बटन पर क्लिक कर सकता है। React फिर hydration के दौरान उस DOM नोड को बदल देता है या पुनर्गठित कर देता है। आपका टेस्ट फ्रेमवर्क जिस एलिमेंट हैंडल को पकड़े हुए था, वह अब एक अलग (detached) नोड की ओर इशारा करता है, और आपको हटाए गए एलिमेंट के साथ इंटरैक्ट करने के बारे में एक त्रुटि मिलती है।
इसका समाधान कंपोनेंट ट्री में गहराई तक जाने वाला अधिक जटिल सेलेक्टर (selector) लिखना नहीं है। समाधान तैयारी के संकेतों (readiness signals) को खोजना है। तब तक प्रतीक्षा करें जब तक कि एक रूट एलिमेंट hydrated attribute या किसी ज्ञात data property को प्राप्त न कर ले। स्केलेटन लोडर (skeleton loader) के गायब होने का इंतज़ार करें। क्लाइंट-साइड इवेंट हैंडलर के सक्रिय होने का इंतज़ार करें। क्लिक करने से पहले एप्लिकेशन को यह घोषित करने दें कि वह स्थिर है।
छिपे हुए अपराधी: डिपेंडेंसीज़ और थर्ड-पार्टी स्क्रिप्ट्स
कभी-कभी वातावरण बदल जाता है भले ही आपके एप्लिकेशन कोड में कोई बदलाव न हुआ हो। आपके node_modules में तीन स्तर गहरे एक छोटी यूटिलिटी लाइब्रेरी में एक ट्रांसिटिव अपडेट (transitive update), ब्राउज़र के व्यवहार को बदल सकता है। यह बदल सकता है कि प्रॉमिस (promises) कैसे हल होते हैं, स्टाइल कैसे इंजेक्ट किए जाते हैं, या मॉक्स (mocks) अनुरोधों को कैसे इंटरसेप्ट करते हैं। जब नियमित डिपेंडेंसी अपडेट के बाद टेस्ट विफल होने लगते हैं, तो अपने पैकेज मैनेजर वर्शन और लॉकफ़ाइल चेकसम (lockfile checksum) को रिकॉर्ड करें। आपको यह जानने की आवश्यकता है कि क्या आप उसी ट्री को देख रहे हैं जिसे आप पिछले सप्ताह देख रहे थे।
थर्ड-पार्टी स्क्रिप्ट्स एक और बार-बार होने वाला व्यवधान हैं। एनालिटिक्स ट्रैकर्स, पेमेंट SDKs, और चैट विजेट्स एसिंक्रोनस रूप से लोड होते हैं। वे उन क्षणों में iframes इंजेक्ट करते हैं, लेआउट बदलते हैं, या फोकस चुरा लेते हैं जिसकी आपका टेस्ट अपेक्षा नहीं करता है। CI में, ये स्क्रिप्ट अधिक धीरे लोड हो सकती हैं, या नेटवर्क प्रतिबंधों के कारण पूरी तरह से लोड होने में विफल हो सकती हैं, जिससे आपका एप्लिकेशन एक अलग त्रुटि-हैंडलिंग पथ का अनुसरण करता है। लॉग करें कि कौन से थर्ड-पार्टी रिसोर्स लोड हुए और उनका HTTP स्टेटस क्या था। यदि CI में पेमेंट iframe को माउंट होने में तीन सेकंड लगते हैं लेकिन आपके तेज़ स्थानीय कनेक्शन पर यह तुरंत लोड हो जाता है, तो आपकी "element not clickable" त्रुटि का अचानक एक स्पष्ट कारण मिल जाता है।
और "element not clickable" कभी भी निदान (diagnosis) नहीं है। यह एक लक्षण (symptom) है। कारण का इलाज करें।
एक एविडेंस किट (Evidence Kit) बनाएं
प्रत्येक CI विफलता कार्रवाई योग्य (actionable) होनी चाहिए। केवल एक स्टैक ट्रेस (stack trace) पर्याप्त नहीं है। आपको एक एविडेंस किट की आवश्यकता है जो किसी अन्य इंजीनियर को, या अगले महीने आपको, यह पुनर्गठित करने की अनुमति दे कि क्या हुआ था।
विफल रन से स्क्रीनशॉट और वीडियो रिकॉर्डिंग रखें। पूरे ब्राउज़र कंसोल आउटपुट को कैप्चर करें, न केवल त्रुटियों को बल्कि चेतावनियों (warnings) को भी। नेटवर्क विफलताओं को लॉग करें, जिसमें 404s, CORS रिजेक्शन और ड्रॉप किए गए कनेक्शन शामिल हैं। उन बिल्ड आईडी और फीचर फ्लैग्स को सुरक्षित रखें जो सक्रिय थे। ठीक उसी क्षण DOM स्नैपशॉट लें जब एसर्शन (assertion) विफल हुआ। एक स्नैपशॉट आपको घटना के बाद HTML संरचना का निरीक्षण करने की अनुमति देता है, बजाय
