পাইথনে ইন্টারেষ্টিং বাগ; একই মেথডে ইউনিট টেস্ট সেটআপ আর টিয়ার-ডাউন

পাইথনে ইন্টারেষ্টিং বাগ; একই মেথডে ইউনিট টেস্ট সেটআপ আর টিয়ার-ডাউন
২৯ নভেম্বর ২০২৩, বুধবার 


কাজের জায়গায় রিসেন্টলি একটা ইন্টারেষ্টিং বাগ ফেস করেছি, যেটা আমি নিজেই আসলে না জেনে তৈরী করেছিলাম। পাইথনে শেখা একটা নতুন জিনিস ব্যবহার করতে যেয়েই হয়তো (?) বাগটা কোডে ইন্ট্রোডিউসড হয়েছে। সেটাই প্রথমে লিখছি। তারপর আরেকটা নতুন জিনিস - যেটাও আসলে নতুন শিখলাম - কী করে একটা ইউনিট টেস্টের আগে সেটআপ (test setup) আর টেস্টের পরবর্তী টিয়ার-ডাউন (tear down) একই মেথডে করা যায়, সেটা লিখবো। খুব ছোট করেই লিখবো - কারণ এখন বেশ রাত, ঘুমানো দরকার। 

প্রথম বিষয়: বাগ 
ধরা যাক, নিচের কোডের মতো, আমাদের একটা সিম্পল {string: string } ডিকশেনারী আছে। আমাদের কাজ হচ্ছে, একটা key দেয়া হলে, তার করেস্পন্ডিং value প্রিন্ট করা - খুব সিম্পল ভাবে নিচের মতো করে কোডটা লেখা যায়: 

ছবি: ১

ছবি ১'র লাইন-১০-এর কোড, একটু pythonic করা যায়, যেখানে একলাইনেই কন্ডিশন চেক, আর ভ্যালু এসাইনমেন্ট করা যায়। যেমন নিচের কোডের মতো [এই ব্যাপারটাই আমি নতুন শিখে ভেবেছিলাম ব্যবহার করি!]:

ছবি: ২
val := myDict.get(user_input) আসলে একটা এসাইনমেন্ট স্টেটমেন্ট। প্রথমে ডিকশনারি থেকে ভ্যালু নিয়ে এসে ভ্যারিয়েবল val -এ রাখবে, তারপর if -কন্ডিশন চেক হবে। অর্থাৎ,  ছবি ১ আর ছবি ২ -এর কোড একই আউটপুট দিবে, কোনো সমস্যা নাই। পার্থক্য শুধু হচ্ছে:  ছবি-২'র কোডে ডিকশেনারীটা  দুইবারের বদলে একবার এক্সেস হচ্ছে। ডিকশনারিটা 'get' করে key'র করেস্পন্ডিং value  নিয়ে এসে ভ্যারিয়েবল val -এ রাখছে। যদি key না থাকে, তাহলে ডিফল্ট None ভ্যালু পাবে। আর if None  যেহেতু false তাই, key না থাকলে else ব্লক, অর্থাৎ লাইন ১২-১৩ এক্সেকিউট হবে। কোনো সমস্যা হওয়ার কথা না। তাই না?

কিন্তু এই দুই কোডেই একটা বাগ আছে। কি ধরতে পারছেন? খুব suttle বা সুক্ষ একটা বাগ। একটা হিন্ট দেই, value  যেহেতু string, কোনো একটা key'র রেস্পেক্টিভ value যদি empty string ("") হয়, তখন কি হবে? নিচের কোডে খেয়াল করুন:

ছবি: ৩
আমরা নতুন একটা key-value পেয়ার অ্যাড করেছি (লাইন ৫), যেখানে value আসলে empty string। যখন, key ("empty") দিয়ে ডিকশেনারী লুক-আপ করছি, করেস্পন্ডিং value ("") থাকার পরও বলছে "Key not found"! কারণ কী? কারণটা হচ্ছে, পাইথনে empty string, empty list কিংবা সংখ্যা শূন্য (0) if -কন্ডিশনে false ইভালুয়েট করে বা রিটার্ন করে! আর তাই,  একটা ভ্যালিড value থাকার পরও if কন্ডিশন true রিটার্ন করছে না।

অর্থাৎ, if []: অথবা  if 0:, কিংবা  if "": - সবগুলাই false রিটার্ন করবে 

আশা করি ব্যাগটা বুঝতে পেরেছেন। 

বাগটা সরাতে আমাদের এক্সপ্লিসিটলি বলে দিতে হবে ভ্যারিয়েবল val যেন None না হয়, যেমন নিচের কোডে:



ছবি:৪
ছবি ৪-এ লাইন ১০-এ ভ্যারিয়েবল val -তে ডিকশনারির ভ্যালু রাখার পর এক্সপ্লিসিটলি চেক করছি যে সেটা যেন not None হয়, তবেই প্রিন্ট করছি। এখন empty string ("") যেহেতু None না, তাই if -কন্ডিশন true হবে, আর প্রিন্ট স্টেটমেন্ট এক্সেকিউট হবে, আমরা "Key not found" -এর পরিবর্তে যা চাচ্ছিলাম, সেই এম্পটি স্ট্রিং আউটপুটে দেখতে পাবো [এম্পটি স্ট্রিং হওয়ায় দেখতে আসলে পারছি না :)] 

খেয়াল রাখতে হবে, ব্রাকেট বা পারেনথেসিস না দিলে কিন্তু আরেকটা বাগ তৈরী হবে; কী সেটা? সেটা না হয় নিজেরাই নিচের লিংকে গিয়ে কোড মোডিফাই করে, কোড রান করে দেখে নিন!

কোড: এই লিংকে 

দ্বিতীয় বিষয়: পাইথন ইউনিট টেস্ট: একই মেথডে সেটআপ আর টিয়ার-ডাউন: একটু জটিল 

কখনো কখনো ইউনিট টেস্ট কিছু সেটআপ করা লাগে। যেমন, ধরা যাক, আপনি একটা মেথড লিখেছেন, যেটা কিনা ডাটাবেস-র কোনো একটা টেবিলে একটা এন্ট্রি আপডেট করবে। এই মেথডের ইউনিট টেস্ট হিসাবে আপনি হয়তো চেক করবেন, মেথড কল করা হলে ডাটাবেসে একটা row updated হয় কিনা। ভালো কথা, কিন্তু ইউনিট টেস্টটা রান করার আগে আপনাকে তো নিশ্চিত করতে হবে যে ওই এন্ট্রিটা ডাটাবেসে আছে, ঠিক না? এন্ট্রি যদি না থাকে, তাহলে আপডেট করবেন কিভাবে? ইউনিট টেস্টটা তো ফেল করবে। আর, এই  এন্ট্রিটা যে আছে, সেটা নিশ্চিত করার কাজটাই আপনাকে শুরুর সেটআপ মেথডে করে নিতে হবে। 

আচ্ছা, যখন আপনার ইউনিট টেস্ট শেষ, এইবার সেটআপ-এ ইন্সার্ট করা ওই এন্ট্রি আপনাকে ডিলিট করতে হবে। এটাই টিয়ার-ডাউন (tear -down) প্রসেস। 

আর এক সেটআপ আর টিয়ার-ডাউন করার দুইটা প্রসেসই একটা মেথড দিয়ে করা যায়! পাইথনে yield কীওয়ার্ড দিয়ে সেটা করা যায়, উদাহরণ হিসাবে নিচের কোডের টেম্পলেট দিচ্ছি:

ছবি: ৫: ইউনিট টেস্ট টেম্পলেট 

পাইথনে @pytest.fixture ডেকরেট ব্যবহার করে আমরা কোনো টেস্ট মেথড রান করার আগে সেটআপ মেথড রান করাতে পারি। ছবি ৫-এ ইউনিট টেস্ট test_method রান করার আগে সবসময় setup_tear_down মেথড কল হবে। 
আর ওই মেথডে প্রথমে ডাটাবেসে এন্ট্রি দিতে হবে। তারপরেই yied কীওয়ার্ড ব্যবহার করে কোডের এক্সেকিউশন caller 'র কাছে ফেরত পাঠানো যায়। Caller অর্থাৎ test_method কাজ শেষ করার পর (টেস্ট মেথড row update count এসার্ট করার পর) আবারো setup_tear_down মেথডে ফেরত আসবে। আর তারপর ডাটাবেস থেকে ওই আপডেটেড এন্ট্রি ডিলিট করে দিলেই আমাদের কাজ শেষ!

আশাকরি  ব্যাপার দুইটা বোঝা গেছে! ধন্যবাদ! 

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম  ৩ ফেব্রুয়ারি, শনিবার, ২০২৪ রেগুলার এক্সপ্রেশন (Regular Exp...