প্রপার্টি-বেসড টেস্টিং
এই পাঠে যা শিখবেন
- প্রপার্টি-বেসড টেস্টিং কী এবং উদাহরণ-ভিত্তিক টেস্টিং থেকে এটি ঠিক কীভাবে আলাদা
- একটি ভালো "প্রপার্টি" বা invariant কীভাবে বেছে নিতে হয়
- stdlib-only
randomদিয়ে বহু ইনপুট জেনারেট করে একটি প্রপার্টি যাচাই করার সত্যিকারের কৌশল - একটি বাগযুক্ত ইমপ্লিমেন্টেশনে কীভাবে একটি প্রকৃত counterexample ধরা পড়ে তার লাইভ ডেমো
১ · প্রপার্টি-বেসড টেস্টিং কী, এবং কেন
এই কোর্সের L01 থেকে L40 পর্যন্ত প্রায় প্রতিটি টেস্ট ছিল
উদাহরণ-ভিত্তিকExample-Based Testingএকটি নির্দিষ্ট ইনপুটের জন্য একটি নির্দিষ্ট প্রত্যাশিত আউটপুট লিখে টেস্ট করা। —
যেমন L01-এ calculate_shipping_cost(1000) ঠিক 0 রিটার্ন করবে কি না তা চেক করা।
এই ধরনের টেস্ট স্পষ্ট ও পড়তে সহজ, কিন্তু একটি সীমাবদ্ধতা আছে — টেস্ট-লেখক যে নির্দিষ্ট কয়েকটি ইনপুট নিজে
ভেবে বেছেছেন, শুধু সেগুলোই যাচাই হয়। ভাবেননি এমন কোনো অস্বাভাবিক ইনপুট কম্বিনেশনে বাগ থাকলে তা ধরা পড়ে না।
প্রপার্টি-বেসড টেস্টিংProperty-Based Testingনির্দিষ্ট ইনপুট-আউটপুট জোড়া না লিখে, একটি সাধারণ নিয়ম (property/invariant) বলে দিয়ে বহু স্বয়ংক্রিয়ভাবে জেনারেট করা ইনপুটে তা যাচাই করা।
ভিন্নভাবে চিন্তা করে — নির্দিষ্ট ইনপুট-আউটপুট জোড়া লেখার বদলে, একটি সাধারণ নিয়ম বা invariant
বলে দেওয়া হয় যা যেকোনো বৈধ ইনপুটের জন্যই সত্য থাকা উচিত, তারপর টুলটি নিজে (এলোমেলোভাবে বা
নিয়মতান্ত্রিকভাবে) বহু ইনপুট বানিয়ে সেই নিয়ম ভাঙে কি না পরীক্ষা করে। বাস্তব-জগতের Python ইকোসিস্টেমে এর জন্য
সবচেয়ে বহুল-ব্যবহৃত টুল হলো hypothesis লাইব্রেরি — এটি স্মার্টভাবে ইনপুট জেনারেট করে (শুধু
র্যান্ডম নয়, edge case-এর দিকে পক্ষপাতমূলক), এবং একটি ব্যর্থতা পেলে সেটিকে "shrink" করে সবচেয়ে ছোট/সরল
counterexample খুঁজে বের করে। এই স্যান্ডবক্সে hypothesis ইনস্টল করা সম্ভব নয়
(software-testing-qa/CLAUDE.md অনুযায়ী), তাই আমরা নিচে stdlib-এর random মডিউল দিয়ে
সেই একই মূল প্রক্রিয়াটির একটি সত্যিকারের, চলমান, সরলীকৃত সংস্করণ বানাব।
২ · একটি ভালো প্রপার্টি বেছে নেওয়া
সব ফাংশনের জন্য প্রপার্টি লেখা সহজ নয় — চ্যালেঞ্জটা হলো এমন একটি নিয়ম খুঁজে বের করা যা (ক) সত্যিই সবসময় সত্য, এবং (খ) নিজে থেকে "সঠিক আউটপুট" পুরোপুরি হার্ডকোড না করেও কিছু অর্থবহ যাচাই করে। কয়েকটি সাধারণ প্যাটার্ন:
একটি অপারেশন করে তার বিপরীত অপারেশন করলে মূল মান ফেরত পাওয়া উচিত — যেমন একটি লিস্ট দুইবার reverse করলে মূল লিস্টই ফেরত আসা উচিত।
অপারেশনের পরও কিছু বৈশিষ্ট্য অপরিবর্তিত থাকা উচিত — যেমন সর্ট করার পর উপাদানের সংখ্যা ও প্রতিটি উপাদানের multiplicity (কয়টি করে আছে) একই থাকা উচিত (পার্মুটেশন)।
আউটপুট একটি নির্দিষ্ট ক্রম মেনে চলা উচিত — যেমন সর্ট করার পর প্রতিটি উপাদান তার পরের উপাদানের চেয়ে ছোট বা সমান হওয়া উচিত (non-decreasing)।
নিচের কোডে আমরা একটি ক্লাসিক জোড়া বেছে নিচ্ছি — bubble_sort ফাংশনের জন্য Ordering
প্রপার্টি ("আউটপুট non-decreasing") এবং Invariant প্রপার্টি ("আউটপুট ইনপুটের একটি পার্মুটেশন,
অর্থাৎ একই উপাদান একই সংখ্যায় আছে") — দুটো একসাথে সত্য হলেই বলা যায় সর্টিং সঠিকভাবে হয়েছে, কারণ শুধু
non-decreasing হলেই যথেষ্ট নয় (যেমন একটি বাগযুক্ত ফাংশন সব উপাদান বাদ দিয়ে খালি লিস্ট রিটার্ন করলেও তা
"non-decreasing" — তাই পার্মুটেশন চেক-টিও লাগবে)।
৩ · বাস্তবায়ন: ২০০টি জেনারেট করা ইনপুটে প্রপার্টি যাচাই
নিচের কোড সেলে একটি সঠিক bubble_sort ফাংশন আছে, এবং একটি ফিক্সড seed (42) দিয়ে
random.Random-এর একটি আলাদা instance তৈরি করে ২০০টি ভিন্ন দৈর্ঘ্যের (০ থেকে ১৫টি উপাদান) র্যান্ডম
পূর্ণসংখ্যার লিস্ট জেনারেট করা হয়েছে। প্রতিটির জন্য দুটো প্রপার্টিই — non-decreasing এবং পার্মুটেশন — সত্যিকারের
লুপ ও assert-স্টাইল চেক দিয়ে যাচাই করা হয়, এবং প্রথম যে ইনপুট নিয়ম ভাঙে তা রিপোর্ট করা হয় (এখানে
কোনোটিই ভাঙবে না, কারণ bubble_sort সঠিকভাবে লেখা)।
import random
from collections import Counter
def bubble_sort(items):
lst = list(items)
n = len(lst)
for i in range(n):
for j in range(0, n - i - 1):
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
return lst
def is_sorted_non_decreasing(lst):
return all(lst[i] <= lst[i + 1] for i in range(len(lst) - 1))
def is_permutation(original, result):
# একই উপাদান, একই সংখ্যায় আছে কি না (multiset তুলনা) — sorted() এর উপর নির্ভর না করে
return Counter(original) == Counter(result)
rng = random.Random(42) # ফিক্সড seed -> প্রতিবার একই ইনপুট, reproducible ফলাফল
NUM_TESTS = 200
counterexample = None
for test_num in range(NUM_TESTS):
length = rng.randint(0, 15)
original = [rng.randint(-50, 50) for _ in range(length)]
result = bubble_sort(original)
if not (is_sorted_non_decreasing(result) and is_permutation(original, result)):
counterexample = (test_num, original, result)
break
if counterexample is None:
print(f"PASS: property held for all {NUM_TESTS} randomly generated lists")
print("(sorted output is non-decreasing AND is a permutation of the input, every time)")
else:
test_num, original, result = counterexample
print(f"FAIL at test #{test_num}: input={original} -> output={result}")
hypothesis-এর মতো টুলের মূল কাজ — শুধু স্কেল আরও বড় এবং ইনপুট
জেনারেশন আরও স্মার্ট (edge case-মুখী)।
৪ · একটি বাগ ধরা: counterexample-এর প্রকৃত উদাহরণ
প্রপার্টি-বেসড টেস্টিং তখনই তার আসল শক্তি দেখায় যখন কোনো বাগ ধরা পড়ে। নিচে একটি ইচ্ছাকৃতভাবে বাগযুক্ত
buggy_sort ফাংশন — এটি ভেতরে একটি Python set ব্যবহার করে, যা নিঃশব্দে ডুপ্লিকেট
উপাদান বাদ দিয়ে দেয়। ফাংশনটি এখনও একটি non-decreasing লিস্ট রিটার্ন করে (তাই শুধু ordering প্রপার্টি চেক
করলে ধরা পড়ত না!), কিন্তু পার্মুটেশন প্রপার্টি ভেঙে যায় যেই মুহূর্তে ইনপুটে কোনো ডুপ্লিকেট মান থাকে — এবং
একই ফিক্সড-seed জেনারেটর দিয়ে চালালে এটি খুব দ্রুতই একটি প্রকৃত counterexample খুঁজে বের করে।
def buggy_sort(items):
# বাগ: set() ব্যবহারের কারণে ডুপ্লিকেট উপাদান নিঃশব্দে হারিয়ে যায়
return sorted(set(items))
rng2 = random.Random(42) # একই seed -> একই ২০০টি ইনপুট, পুনরুৎপাদনযোগ্য ব্যর্থতা
counterexample2 = None
for test_num in range(NUM_TESTS):
length = rng2.randint(0, 15)
original = [rng2.randint(-50, 50) for _ in range(length)]
result = buggy_sort(original)
if not (is_sorted_non_decreasing(result) and is_permutation(original, result)):
counterexample2 = (test_num, original, result)
break
if counterexample2 is None:
print(f"PASS: property held for all {NUM_TESTS} randomly generated lists")
else:
test_num, original, result = counterexample2
print(f"FAIL at test #{test_num}: input={original} -> output={result}")
print("কারণ: ইনপুটে থাকা একটি ডুপ্লিকেট মান আউটপুটে একবারই আছে -> পার্মুটেশন প্রপার্টি ভেঙেছে")
FAIL রিপোর্ট হয় প্রথম দিকের একটি টেস্টেই — কারণ ইনপুট লিস্টে একটি সংখ্যা দুইবার
থাকলে (যেমন 44 দুইবার), set() সেটিকে একবারে নামিয়ে আনে, তাই আউটপুটের দৈর্ঘ্য
ইনপুটের চেয়ে ছোট হয়ে যায় — Counter তুলনা এটি সাথে সাথে ধরে ফেলে। লক্ষ্য করুন প্রথম সেলের মতোই
seed 42 ব্যবহার করা হয়েছে, তাই এই ব্যর্থতা প্রতিবার একই জায়গায় ঘটবে —
reproducibility ছাড়া একটি ব্যর্থ প্রপার্টি টেস্ট ডিবাগ করা কঠিন হয়ে যেত।
প্রপার্টি-বেসড টেস্টিং M3-এর উদাহরণ-ভিত্তিক ইউনিট টেস্টিং-কে প্রতিস্থাপন করে না — বরং তার পরিপূরক, কারণ এটি অনেক বেশি ইনপুট কভার করে যা হাতে লিখে কল্পনা করা কঠিন। পরের পাঠ (L42, মিউটেশন টেস্টিং)-এ আমরা উল্টো প্রশ্ন করব — "আমাদের টেস্ট স্যুট নিজে কতটা ভালো, বাগ ধরতে সক্ষম?"
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
শুধু "আউটপুট non-decreasing" প্রপার্টি চেক করলে buggy_sort-এর বাগ কেন ধরা পড়ত না?
কারণ buggy_sort যা রিটার্ন করে তা আসলেই non-decreasing (ডুপ্লিকেট বাদ দেওয়া একটি ছোট লিস্টও
সাজানো অবস্থায় থাকতে পারে)। একটি একক দুর্বল প্রপার্টি পুরো বাগ ধরতে পারে না — এই কারণেই দুটো স্বাধীন
প্রপার্টি (ordering + পার্মুটেশন) একসাথে ব্যবহার করা হয়েছে, ঠিক যেমন ইউনিট টেস্টেও একটি দুর্বল
assert (যেমন শুধু "ক্র্যাশ করেনি" চেক করা) প্রকৃত বাগ মিস করতে পারে (M6/L26-এ বিস্তারিত)।
প্র ০২
একই কোডে random.seed(42)-এর বদলে seed না দিয়ে সরাসরি random.randint(...)
ব্যবহার করলে কী সমস্যা হতো?
প্রতিবার কোড চালালে ভিন্ন ইনপুট জেনারেট হতো — একবার একটি বাগ ধরা পড়লেও পরের বার হয়তো একই কোড আবার চালালে বাগটি ধরা পড়ত না (কারণ যে নির্দিষ্ট ডুপ্লিকেট-সহ ইনপুটটি বাগ প্রকাশ করেছিল সেটি হয়তো আবার জেনারেট হবে না)। এতে ফলাফল অপুনরুৎপাদনযোগ্য (non-reproducible) হয়ে যেত, এবং ডিবাগ করা কঠিন — এই সমস্যাটিকেই M7/L30-এ "flaky test"-এর একটি কারণ হিসেবে আলোচনা করা হবে।
প্র ০৩
বাস্তব hypothesis লাইব্রেরি "shrinking" নামের একটি ফিচার দেয় — এটি মোটামুটি কী করে
বলে আপনার মনে হয়, এবং এটি কেন উপকারী?
একটি বড় বা জটিল counterexample পাওয়ার পর, shrinking স্বয়ংক্রিয়ভাবে সেটিকে ছোট ও সরল করার চেষ্টা করে
(যেমন ৫০টি উপাদানের একটি ব্যর্থ লিস্ট থেকে ধীরে ধীরে উপাদান বাদ দিয়ে দেখা, তাও কি ব্যর্থ হয়) যতক্ষণ না
আর ছোট করা যায় এমন একটি ন্যূনতম ব্যর্থ কেস পাওয়া যায়। এটি উপকারী কারণ একটি ছোট, সরল counterexample
(যেমন [5, 5]) ডিবাগ করা অনেক সহজ একটি বড়, এলোমেলো লিস্টের চেয়ে। এই পাঠের সরলীকৃত সংস্করণে
shrinking বাস্তবায়ন করা হয়নি — শুধু প্রথম যে ইনপুট ব্যর্থ হয় তা সরাসরি রিপোর্ট করা হয়েছে।
অনুশীলন
-
চিন্তা করুন:
reverse_listনামের একটি ফাংশন (একটি লিস্ট উল্টে দেয়) এর জন্য কী একটি ভালো round-trip প্রপার্টি হতে পারে?"যেকোনো লিস্ট
lst-এর জন্য,reverse_list(reverse_list(lst)) == lstসবসময় সত্য হওয়া উচিত" — দুইবার উল্টালে মূল লিস্টই ফেরত পাওয়া উচিত। এটি একটি ক্লাসিক round-trip প্রপার্টি, যা অনেক ফাংশনের জন্য (encode/decode, serialize/deserialize, sort/is-already-sorted-check ইত্যাদি) ব্যবহার করা যায়। -
পরীক্ষা করুন: প্রথম কোড সেলে
NUM_TESTS = 200-কেNUM_TESTS = 2000-এ পরিবর্তন করে Run চেপে দেখুন — ফলাফল কি এখনও PASS আসে?হ্যাঁ, এখনও
PASSআসা উচিত — কারণbubble_sortফাংশনটি সত্যিই সঠিক, তাই যত বেশি ইনপুটেই চালানো হোক না কেন প্রপার্টি ভাঙবে না। বাস্তবhypothesis-এও এই নীতিই কাজ করে — বেশি জেনারেট করা ইনপুট মানে সঠিক কোডের প্রতি আরও বেশি ভরসা (কিন্তু কখনোই ১০০% প্রমাণ নয়, কারণ সব সম্ভাব্য ইনপুট কখনো শেষ হয় না — M1/L04-এর "exhaustive testing is impossible" নীতির সাথে সরাসরি সম্পর্কিত)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ পড়ুন মিউটেশন টেস্টিং আপনার টেস্ট স্যুট নিজে কতটা ভালো তা কীভাবে পরিমাপ করবেন — ইচ্ছাকৃতভাবে বাগ ঢুকিয়ে দেখা সেটি ধরা পড়ে কি না।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, কভারেজ, অটোমেশন, পারফরম্যান্স, সিকিউরিটি ও অ্যাডভান্সড টেকনিক — সব একসাথে।
- সব 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 — সব এক জায়গায়।