ফ্লেকি টেস্ট — কারণ ও সমাধান
এই পাঠে যা শিখবেন
- ফ্লেকি টেস্ট কী, এবং কেন এটি একটি টেস্ট স্যুটের সবচেয়ে বিপজ্জনক সমস্যাগুলোর একটি
- ফ্লেকিনেসের চারটি সাধারণ কারণ, প্রতিটির একটি বাস্তব উদাহরণসহ
- একটি সত্যিকারের, বারবার-ভিন্ন-ফলাফল-দেওয়া টেস্ট দেখা এবং কেন এটি ঘটে
- seeding দিয়ে randomness নিয়ন্ত্রণ করে কীভাবে একই টেস্টকে ডিটারমিনিস্টিক বানানো যায়
১ · ফ্লেকিনেসের চারটি সাধারণ কারণ
টেস্ট ধরে নেয় একটি asynchronous অপারেশন নির্দিষ্ট সময়ের মধ্যে শেষ হবে (যেমন একটি ফিক্সড
sleep), কিন্তু বাস্তব সময় সিস্টেম লোডের উপর নির্ভর করে বদলাতে পারে।একটি টেস্ট আগের টেস্টের রেখে যাওয়া ডেটা/স্টেটের উপর নির্ভর করে ফেলে (M3/L12-এ setUp/tearDown বিস্তারিত) — টেস্ট আলাদাভাবে চালালে ফেইল করে, একসাথে নির্দিষ্ট অর্ডারে চালালে পাস করে।
টেস্ট একটি real API/ডাটাবেস/নেটওয়ার্কের উপর নির্ভর করে, যা মাঝে মাঝে ধীর বা সাময়িকভাবে অনুপলব্ধ হতে পারে (M4/L16-এ স্টাব/ফেক দিয়ে এই নির্ভরতা দূর করার আলোচনা)।
টেস্ট এলোমেলো মান ব্যবহার করে কিন্তু কোনো fixed seed সেট করে না, অথবা টেস্ট আউটপুট এমন একটি কালেকশনের (যেমন
set) উপর নির্ভর করে যার iteration order গ্যারান্টিড নয়।২ · একটি সত্যিকারের ফ্লেকি টেস্ট — সরাসরি দেখুন
নিচের কোড সেলে একটি "নেটওয়ার্ক রেসপন্স টাইমআউটের মধ্যে এসেছে কি না" চেক করা একটি টেস্ট লজিক আছে। এটি
random.uniform(0, 100) দিয়ে একটি সিমুলেটেড লেটেন্সি (ms) তৈরি করে এবং TIMEOUT_MS-এর
সাথে তুলনা করে — কিন্তু কোনো seed সেট না করেই। একই লজিক ১০ বার চালানো হয়েছে; যেহেতু Python-এর গ্লোবাল random
জেনারেটর প্রতিবার নতুন মান দেয়, ফলাফল সত্যিই রান থেকে রানে ভিন্ন হতে পারে।
import random
TIMEOUT_MS = 50
def flaky_response_within_timeout():
"""ত্রুটিপূর্ণ ডিজাইন: প্রতিবার গ্লোবাল random state থেকে টানা হয়, কোনো seed নিয়ন্ত্রণ নেই।"""
latency = random.uniform(0, 100) # সিমুলেটেড নেটওয়ার্ক লেটেন্সি (ms)
return latency < TIMEOUT_MS, latency
print("ফ্লেকি টেস্ট লজিক — একই assertion ১০ বার চালানো হলো, কোনো seed ছাড়াই:")
flaky_results = []
for i in range(1, 11):
passed, latency = flaky_response_within_timeout()
flaky_results.append(passed)
status = "PASS" if passed else "FAIL"
print(f" রান {i:2d}: লেটেন্সি = {latency:6.2f}ms → {status}")
pass_count = sum(flaky_results)
print(f"\nমোট {len(flaky_results)}টি রানের মধ্যে PASS হয়েছে {pass_count}টি, FAIL হয়েছে {len(flaky_results) - pass_count}টি।")
print("কোড একবারও বদলায়নি, একই assertion — শুধু কখন চালানো হচ্ছে তার এলোমেলো ফলাফলের উপর ভিত্তি করে পাস/ফেইল বদলে যাচ্ছে।")
print("(এই সেলটি বারবার Run করলে উপরের সংখ্যাগুলো প্রতিবার ভিন্ন হতে পারে — এটাই ফ্লেকিনেসের সংজ্ঞা।)")
# ---------- ফিক্সড সংস্করণ: প্রতিটি রানের আগে একই নির্দিষ্ট seed দিয়ে একটি স্বতন্ত্র RNG তৈরি ----------
def fixed_response_within_timeout():
"""ঠিক করা সংস্করণ: প্রতিবার একই seed(7) দিয়ে একটি নতুন, স্বতন্ত্র RNG থেকে মান নেওয়া হয়।"""
rng = random.Random(7) # গ্লোবাল random state-এর উপর নির্ভর করে না, প্রতিবার একই বিন্দু থেকে শুরু
latency = rng.uniform(0, 100)
return latency < TIMEOUT_MS, latency
print("\nফিক্সড টেস্ট লজিক — একই assertion ১০ বার চালানো হলো, প্রতিবার seed(7) দিয়ে:")
fixed_results = []
for i in range(1, 11):
passed, latency = fixed_response_within_timeout()
fixed_results.append(passed)
status = "PASS" if passed else "FAIL"
print(f" রান {i:2d}: লেটেন্সি = {latency:6.2f}ms → {status}")
print(f"\nমোট {len(fixed_results)}টি রানের মধ্যে PASS হয়েছে {sum(fixed_results)}টি — এবং লক্ষ্য করুন প্রতিটি রানের "
f"লেটেন্সি মান হুবহু একই, কারণ প্রতিবার একই seed(7) দিয়ে RNG নতুন করে শুরু হচ্ছে।")
flaky_response_within_timeout) প্রতিটি রান গ্লোবাল random মডিউলের
চলমান অবস্থা থেকে একটি নতুন মান টানে — তাই ১০টি রানের মধ্যে কতগুলো PASS আর কতগুলো FAIL হবে তা আগে থেকে বলা যায়
না, এবং সেলটি আবার Run করলে ভিন্ন সংখ্যা আসতে পারে। দ্বিতীয় অংশে (fixed_response_within_timeout)
প্রতিটি কলে random.Random(7) দিয়ে একটি নতুন, স্বতন্ত্র RNG তৈরি করা হয়েছে, একই
seed দিয়ে — ফলে প্রতিটি কল ঠিক একই লেটেন্সি মান দেয়, এবং ১০টি রানের ফলাফল হুবহু অভিন্ন (সব PASS অথবা সব FAIL,
যেটাই হোক)। গুরুত্বপূর্ণ বিষয়টি হলো ফলাফল পাস হওয়া নয় — গুরুত্বপূর্ণ হলো এটি প্রতিবার
একই ফলাফল দেয়। একটি নির্ভরযোগ্যভাবে ফেইল-হওয়া টেস্টও একটি ফ্লেকি টেস্টের চেয়ে ভালো, কারণ
সেটি অন্তত ডিবাগ করা যায়।
৩ · অন্যান্য সাধারণ সমাধান
randomness seed করা শুধু একটি উদাহরণ। বাস্তবে দলগুলো একই নীতি প্রয়োগ করে অন্য কারণগুলোর জন্যও: টাইমিং সমস্যার
জন্য ফিক্সড sleep-এর বদলে "শর্ত সত্যি না হওয়া পর্যন্ত অপেক্ষা করো" ধরনের পোলিং/ওয়েট ব্যবহার করা
(M7/L31-এ বিস্তারিত), শেয়ার্ড স্টেটের জন্য প্রতিটি টেস্টে ফ্রেশ ফিক্সচার তৈরি করা (M3/L12), আর বাহ্যিক সার্ভিসের
জন্য টেস্ট ডাবলস দিয়ে সেই নির্ভরতা প্রতিস্থাপন করা (M4)।
একটি ফ্লেকি টেস্ট আসলে বাগ ধরার বদলে বিভ্রান্তি তৈরি করে — দল "আবার চালিয়ে দেখি" বলে ফেইলিওর উপেক্ষা করতে শেখে (M13/L56-এ এই অ্যান্টি-প্যাটার্ন বিস্তারিত), যা আসল বাগও উপেক্ষা করার অভ্যাস তৈরি করে। প্রতিটি ফ্লেকিনেসের কারণ আসলে একই মূলনীতির লঙ্ঘন — টেস্টের ফলাফল শুধু কোড ও তার ইনপুটের উপর নির্ভর করা উচিত, আর কিছুর উপর নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের ফিক্সড সংস্করণে random.Random(7) প্রতিবার নতুন করে তৈরি করা হয়েছে,
একই ফাংশনের একবার তৈরি করে বারবার ব্যবহার করা হয়নি কেন?
যদি একটি একক random.Random(7) ইনস্ট্যান্স তৈরি করে বারবার তার .uniform() কল করা
হতো, প্রতিটি কল RNG-এর অভ্যন্তরীণ অবস্থা এগিয়ে নিত, তাই প্রতিটি কল ভিন্ন মান দিত — ঠিক গ্লোবাল
random-এর মতোই অ-নির্ধারিত আচরণ হতো। প্রতিটি "টেস্ট রান"-কে সম্পূর্ণ স্বতন্ত্র রাখতে,
প্রতিবার নতুন ইনস্ট্যান্স একই seed দিয়ে তৈরি করা হয়েছে — এটাই টেস্ট আইসোলেশনের মূলনীতি (M3/L12)
randomness-এর ক্ষেত্রে প্রয়োগ করা।
প্র ০২ একটি টিম একটি ফ্লেকি E2E টেস্ট দেখে সিদ্ধান্ত নেয় "মাঝে মাঝে ফেইল করে, এটাকে স্কিপ/ignore করে দিই" — এই সিদ্ধান্তে দীর্ঘমেয়াদে কী ঝুঁকি আছে?
স্কিপ করা টেস্ট কার্যকরভাবে সেই কভারেজ হারিয়ে ফেলে (M6/L26-এর মতোই — একটি না-চালানো টেস্ট কোনো প্রকৃত যাচাই দেয় না)। আরও খারাপ, যদি আসল কারণ (যেমন একটি সত্যিকারের রেস কন্ডিশন বাগ) কখনো ঠিক না করা হয়, ভবিষ্যতে আরও ফ্লেকি টেস্ট জমতে থাকে এবং পুরো স্যুটের উপর আস্থা ধীরে ধীরে শূন্যে নেমে আসে — এটি M13/L56-এ আলোচিত একটি পরিচিত অ্যান্টি-প্যাটার্ন।
প্র ০৩ শেয়ার্ড/অবশিষ্ট স্টেট থেকে আসা ফ্লেকিনেস কেন প্রায়ই "আমার মেশিনে তো কাজ করে" ধরনের সমস্যা তৈরি করে?
কারণ এই ধরনের ফ্লেকিনেস প্রায়ই টেস্ট চালানোর অর্ডার-এর উপর নির্ভর করে। একটি ডেভেলপারের মেশিনে টেস্ট রানার হয়তো একটি নির্দিষ্ট অর্ডারে টেস্ট চালায় যেখানে প্রয়োজনীয় "leftover" স্টেট কাকতালীয়ভাবে ঠিক জায়গায় থাকে, কিন্তু CI সার্ভারে ভিন্ন অর্ডারে (বা প্যারালালে) চালালে সেই স্টেট অনুপস্থিত থাকে — তাই টেস্ট স্থানীয়ভাবে পাস করে কিন্তু CI-তে ফেইল করে।
অনুশীলন
-
চিন্তা করুন: আপনার কোনো প্রজেক্টে (বা কল্পনা করা একটি প্রজেক্টে) এমন একটি টেস্ট পরিস্থিতি
ভাবুন যা উপরের চারটি কারণের একটির শিকার হতে পারে। সেটি কীভাবে ঠিক করা যেত?
সাধারণ উদাহরণ: "সর্বশেষ তৈরি হওয়া ব্যবহারকারী আইডি সবচেয়ে বড় সংখ্যা হবে" ধরে নেওয়া একটি টেস্ট — যদি টেস্ট ডাটাবেস অন্য টেস্ট থেকে অবশিষ্ট ডেটা ধরে রাখে (শেয়ার্ড স্টেট), এই ধারণা ভুল প্রমাণিত হতে পারে। সমাধান: প্রতিটি টেস্টে
setUp()-এ একটি ফ্রেশ, বিচ্ছিন্ন ডেটাসেট তৈরি করা (M3/L12)। -
পরীক্ষা করুন: উপরের কোড সেলে
TIMEOUT_MS-কে50থেকে90এ পরিবর্তন করে Run চেপে দেখুন ফ্লেকি অংশের ফলাফলের ধরন ও ফিক্সড অংশের ফলাফল কীভাবে বদলায়।ফ্লেকি অংশে থ্রেশহোল্ড ৯০ হলে বেশিরভাগ (গড়ে প্রায় ৯০%) র্যান্ডম লেটেন্সি তার নিচে পড়বে, তাই PASS-এর সংখ্যা বাড়বে কিন্তু তারপরও এলোমেলো — এখনও একটি নির্দিষ্ট রান কী দেবে তা নিশ্চিতভাবে বলা যায় না। ফিক্সড অংশে যেহেতু seed(7)-এর লেটেন্সি একটি নির্দিষ্ট মান, থ্রেশহোল্ড বদলালে হয় সবগুলো PASS নয়তো সবগুলো FAIL দেখাবে — কিন্তু ১০টি রান জুড়ে সবসময় একই থাকবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
-
Python Programming কোর্স সহোদর কোর্স
randomমডিউলের ভাষাগত ভিত্তি সেই কোর্সেই ধরে নেওয়া হয়েছে। - সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety ও Software Testing & Quality Assurance — সব এক জায়গায়।