টেস্ট ড্রাইভেন ডেভেলপমেন্টের একটি সংক্ষিপ্ত ধারণা।
টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD) বলতে প্রোগ্রামিংয়ের এমন একটি ধরনকে বোঝায়, যেখানে কোডে প্রোগ্রাম ডিজাইনের বাস্তবায়নকে পথ দেখানোর জন্য টেস্ট লেখা হয়।
কোড লেখার আগে এক বা একাধিক টেস্ট (বিশেষত ইউনিট টেস্ট) লেখা হয়। টেস্টগুলোর উদ্দেশ্য হলো প্রোগ্রামের আচরণের একটি দিক কভার করা, যা একটি একক ফাংশন বা মেথডে কেন্দ্রীভূত হতে পারে। টেস্ট লেখা হলো প্রোগ্রামের প্রয়োজনীয়তা ও সাধারণ আর্কিটেকচারকে একটি বাস্তবায়ন-নির্দিষ্ট ডিজাইনে রূপান্তর করার একটি উপায়। টেস্টগুলো রান করা হয়, এবং সেগুলো ফেল করা উচিত, কারণ কোডটি তখনো বাস্তবায়িত হয়নি। এরপর কোডটি বাস্তবায়িত করা হয় এবং টেস্টগুলো আবার রান করা হয়। টেস্টগুলো পাস করলে হয় ওই আচরণের বাস্তবায়ন সম্পন্ন হয়েছে, নয়তো হয়তো আরও প্রয়োজনীয় টেস্ট তৈরি করা বাকি আছে। টেস্টগুলো পাস না করলে কোডটি ডিবাগ করা হয় এবং টেস্টগুলো আবার রান করা হয়। প্রয়োজনীয় সব টেস্ট পাস না হওয়া পর্যন্ত টেস্টিং ও কোডিংয়ের এই চক্রটি পুনরাবৃত্তি করা হয়; তখন প্রোগ্রামের আচরণের ওই দিকটির বাস্তবায়ন সম্পন্ন হয়... আপাতত।
রিফ্যাক্টরিং হলো কোডের ডিজাইন উন্নত করার জন্য কোডটি আবার লেখা। এটি কেবল বাগ ঠিক করার জন্য কোড আবার লেখা নয়। কখনো কখনো টেস্ট পাস করানোর জন্য কোড পরিবর্তন করাকেও "রিফ্যাক্টরিং" বলা হয়। টেস্ট পাস করানোর উপায় হিসেবে কোড পরিবর্তনের মধ্যে ডিজাইন উন্নত করাও হতে পারে, তবে শুধু ডিবাগ করা মানেই কোডের ডিজাইন উন্নত করা নয়, আর তাই সেটি রিফ্যাক্টরিংও নয়।
নিচেরটি রিফ্যাক্টরিং ছাড়া ডিবাগিংয়ের একটি উদাহরণ:
# A function intended to return x added to y.
# x and y are bad parameter names, but we ignore that for now.
def add(x, y):
# used multiply operator by mistake. It fails the tests.
return x * y
# Function corrected. It passes the tests. It has been debugged, but not refactored.
def add(x, y):
return x + y
নিচেরটি রিফ্যাক্টরিং এবং তারপর ডিবাগিংয়ের একটি উদাহরণ:
# Function name and parameter names are modified to something more meaningful. This is refactoring.
def lot_inventory(old_cars, new_cars):
# Introduced multiply operator by mistake. It fails the tests. This is why we test.
return old_cars * new_cars
# Function corrected. It passes the tests. This is debugging.
def lot_inventory(old_cars, new_cars):
return old_cars + new_cars
Exercism-এর Python ট্র্যাক তার অনুশীলনীগুলোতে TDD পদ্ধতি ব্যবহার করে। ইউনিট টেস্ট আগেই লেখা থাকে। শিক্ষার্থীরা টেস্টগুলো দেখে আরও বিশদভাবে বুঝতে পারেন, একটি সমাধান পাস করতে কী কী প্রয়োজন। শিক্ষার্থীকে একটি সলিউশন স্টাব দেওয়া হতে পারে।
Python সমাধানের এক বা একাধিক টেস্ট ফেল করলে সংশ্লিষ্ট টাস্ক(গুলো)র পটভূমি সবুজ হবে না। প্রথম যে টাস্ক এলাকাটি ফেল করেছে সেটি প্রসারিত করা থাকবে এবং তার হেডার দেখতে অনেকটা এমন হবে
Task 1 Extract coordinates -
মাইনাস চিহ্নে ক্লিক করলে টাস্কটি সংকুচিত হয়ে যায়, ফলে অন্য টাস্কগুলো দেখা যায়; তবে আপাতত আমরা এই টাস্কটিই ধরে রাখব।
নিচে একটি প্রসারিত Test এলাকা থাকবে যা দেখতে অনেকটা এমন হবে
Test 1 ⌄
FAILED TisburyTreasure > get coordinate
এখানে Tisbury Treasure বোঝায় অনুশীলনীটি, আর get_coordinate বোঝায় ফেল করা ফাংশন বা মেথড।
Test 1 সাধারণত একধরনের টেমপ্লেট, যেখানে টেস্ট সেট আপ করার জন্য একটি কোড অংশ থাকে।
এতে কোন নির্দিষ্ট টেস্টটি ফেল করেছে তার তথ্য থাকে না।
এটি নিচের দিকে বলবে
One or more variations of this test failed. Details can be found under each [variant#].
⌄ চিহ্নে ক্লিক করলে টেস্টটি সংকুচিত হবে।
নিচে একটি সংকুচিত টেস্ট থাকবে যা দেখতে অনেকটা এমন:
Test 2 >
FAILED TisburyTreasure > get coordinate [variation #1] (item=
("Scrimshaw Whale's Tooth", '2A'), result='2A')
ডান দিকের প্যানের প্রস্থ অনুযায়ী এটি দেখতে ভিন্ন হতে পারে।
> চিহ্নে ক্লিক করলে টেস্টটি প্রসারিত হবে।
ইনপুট ডেটা এবং প্রত্যাশিত ফলাফলের ডেটা সম্ভবত একটি কোড অংশে দেখানো হবে।
ডেটাটি এই টাস্কের সব টেস্টের জন্য হতে পারে।
একেবারে নিচে, Test Failure অংশে, এই টেস্টটি কেন ফেল করেছে তার নির্দিষ্ট কারণ থাকে।
এটি দেখতে অনেকটা এমন হতে পারে:
AssertionError: ['2A'] != '2A'
এই বিশেষ ক্ষেত্রে, এটি বোঝায় যে ['2A'] রিটার্ন করা মানটি প্রত্যাশিত '2A' মানের সমান হয়নি।
get_coordinate-এর কোডটি দেখলে দেখা যায় এটি এভাবে বাস্তবায়িত হয়েছে
def get_coordinate(record):
return [record[1]]
লিস্টের বন্ধনীগুলো সরিয়ে দিলে (যেমন return record[1]) এবং আবার টেস্ট রান করলে Task 1-এর টেস্টগুলো পাস করবে।
এক বা একাধিক টাস্ক ফেল করা অবস্থায় থাকলে, সব টেস্ট পাস না হওয়া পর্যন্ত প্রতিটির জন্য উপরের প্রক্রিয়াটি পুনরাবৃত্তি করা হয়।
কখনো কখনো প্রত্যাশিত ডেটা ও রিটার্ন করা ডেটা এত বড় হয় যে সেগুলো পুরোটা Test Failure অংশে রাখা যায় না।
এটি দেখতে অনেকটা এমন হতে পারে:
AssertionError: '("Sc[67 chars]\')\n\n(\'Brass Spyglass\', \'Abandoned Lighth[952 chars]')\n' != '("Sc[67 chars]\')\n(\'Brass Spyglass\', \'Abandoned Lighthou[928 chars]')\n'
Diff is 970 characters long. Set self.maxDiff to None to see it.
তবুও সমস্যাটি কী তা বোঝার মতো যথেষ্ট ডেটা থাকতে পারে।
উপরের ক্ষেত্রে, দুটি লাইন ফিড রিটার্ন হয়েছে (যেমন \n\n(\'Brass Spyglass), অথচ প্রত্যাশিত ছিল কেবল একটি (যেমন \n(\'Brass Spyglass)।
অভিনন্দন! সব টেস্ট পাস করেছে। এরপর কী? সমাধানটি এখনই প্রকাশ করা যেতে পারে। অথবা, এখন যেহেতু কোডটি কাজ করছে, আপনি যদি যেকোনো কারণে এটি রিফ্যাক্টর করতে চান, তাহলে কোড পরিবর্তন করে আরেকটি ইটারেশন সাবমিট করতে পারেন। আপনার মনে হলে কোডটি আরও ভালো হতে পারে, কিন্তু কীভাবে তা জানেন না, তাহলে সমাধানটির জন্য মেন্টরিং চাইতে পারেন। কোনো মেন্টর পাওয়া গেলে, তিনি সমাধানের অন্য পদ্ধতির আইডিয়া নিয়ে আপনার সাথে যোগাযোগ করতে পারেন। আপনার সমাধান প্রকাশ করার সময় আপনি মন্তব্য চালু করতে পারেন, আর অন্য শিক্ষার্থীরা সুযোগটি নিয়ে মন্তব্য পোস্ট করতে বা প্রশ্ন জিজ্ঞাসা করতে পারে।
"অকালীন অপ্টিমাইজেশন সব মন্দের মূল" (এ কথাটি Tony Hoare ও Donald Knuth দুজনের নামেই প্রচলিত) — তা সত্ত্বেও এমন সময় আসে, যখন সমাধান কাজ করলেও সমাধানের পারফরম্যান্স উন্নত করার ইচ্ছা হয়।
এমন একটি সময় হতে পারে যখন সমাধান কিছু টেস্ট পাস করে কিন্তু অন্যগুলোতে টাইম আউট হয়ে যায়।
কোডের একটি অংশ ঠিক কত সময় নিচ্ছে তা জানা সহায়ক হতে পারে।
খুব ক্ষুদ্র সময়সীমা পর্যন্ত কোডের এক্সিকিউশনের সময় মাপতে timeit মডিউল ব্যবহার করা যায়।
timeit ফাংশনটি সর্বোচ্চ পাঁচটি আর্গুমেন্ট নিতে পারে: timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)।
stmt প্যারামিটার নির্ধারণ করে কোন আসল কোডটি রান করা হবে ও সময় মাপা হবে।
number প্যারামিটার নির্ধারণ করে stmt কোডটি কতবার রান করা হবে।
setup প্যারামিটার নির্ধারণ করে কোন কোডটি stmt কোড রান করার প্রস্তুতি হিসেবে কেবল একবার রান করা হয়।
setup কোড রান করার সময় সামগ্রিক সময়ের মধ্যে ধরা হয়।
stmt কোড যত বেশি ইটারেশন রান করা হয়, প্রতি ইটারেশনে setup-এর সময় তত কম হিসাবে ধরা হয়।
timer প্যারামিটারের মাধ্যমে ডিফল্টের চেয়ে ভিন্ন একটি Timer পাঠানো যায়।
timer প্যারামিটারের ডিফল্ট আর্গুমেন্ট হলো perf_counter, যা বেশিরভাগ ক্ষেত্রে যথেষ্ট হওয়া উচিত।
number প্যারামিটারের ডিফল্ট আর্গুমেন্ট হলো 1_000_000।
globals প্যারামিটার নির্ধারণ করে কোন নেমস্পেসে কোডটি এক্সিকিউট করা হবে।
globals প্যারামিটারের ডিফল্ট আর্গুমেন্ট হলো None।
একটি বাক্যে ইংরেজি সব স্বরবর্ণ আছে কি না তা নির্ধারণ করতে কত সময় লাগে, তা দেখতে timeit ব্যবহারের একটি উদাহরণ নিচে:
import timeit
# run one million times
loops = 1_000_000
# first positional argument is for stmt
# second positional argument is for setup
# third (named) argument is for number
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
return all(letter in sentence.casefold() for letter in VOWELS)
""", number=loops) / loops)
কোডটি দশ লক্ষবার রান করতে প্রতি কলে গড়ে 4.965089999896008e-07 সেকেন্ড সময় লেগেছে (প্রতি কলে প্রায় 497 ন্যানোসেকেন্ড)।
লিস্ট কম্প্রিহেনশনের বাইরে casefold কলটি সরালে কোনো সময় বাঁচে কি না তা দেখতে নিচের উদাহরণটি:
import timeit
loops = 1_000_000
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
sentence = sentence.casefold()
return all(letter in sentence for letter in VOWELS)
""", number=loops) / loops)
কোডটি দশ লক্ষবার রান করতে প্রতি কলে গড়ে 4.923898000270128e-07 সেকেন্ড সময় লেগেছে (প্রতি কলে প্রায় 492 ন্যানোসেকেন্ড।)
সুতরাং, লিস্ট কম্প্রিহেনশনের বাইরে casefold সরানোর ফলে প্রতি কলে প্রায় 5 ন্যানোসেকেন্ড সময় বেঁচেছে, অর্থাৎ দশ লক্ষ কলে মোট প্রায় 5 মিলিসেকেন্ড।
কোড প্রোফাইল করতেও cProfile ব্যবহার করা যায়; তবে এটি তত সূক্ষ্ম নয়, কারণ এটি কেবল মিলিসেকেন্ড পর্যায় পর্যন্ত যায়।