এই পর্বে যা শিখবেন
- কেন test লিখবেন, আর unit, integration ও end-to-end test-এর পার্থক্য
- কম কিন্তু শক্তিশালী test case বাছাই — equivalence class ও boundary value
- শুধু C আর preprocessor দিয়ে নিজের test harness বানানো
- এমনভাবে কোড লেখা যাতে test করা সহজ হয় — আর TDD-এর red → green → refactor চক্র
- উদাহরণের বাইরে: fuzzing, property, coverage, CI — আর AI-এর লেখা test যাচাইয়ের চেকলিস্ট
১ · Test মানে কোডে লেখা প্রতিশ্রুতি
আগের পর্বে (পর্ব ৩১) আপনি debugger আর sanitizer দিয়ে বাগ খুঁজেছেন। Debugging হলো আগুন নেভানো — বাগ চোখে পড়ার পরে তার উৎস খোঁজা। এই পর্বের বিষয় তার উল্টো দিক: এমন ব্যবস্থা বানানো, যাতে বাগ ব্যবহারকারীর কাছে পৌঁছানোর আগেই ধরা পড়ে, আর একবার ঠিক করা বাগ আর কখনো ফিরে না আসে।
একটি দৃশ্য ভাবুন। নাদিয়া একটি ছোট ফলাফল-প্রকাশের প্রোগ্রাম লিখছেন। নম্বর দিলে গ্রেড দেয়। তিনি ৫০ দিলেন — ঠিক এলো। ৯০ দিলেন — ঠিক। “কাজ শেষ!” পরে দেখা গেল, ঠিক ৮০ পাওয়া শিক্ষার্থীরা A পাচ্ছে না, কারণ শর্তে >=-এর বদলে > লেখা। নাদিয়া হাতে কয়েকবার চালিয়েছিলেন, কিন্তু সীমানার মানটি কখনো দেননি। আর পরের সপ্তাহে কোড বদলানোর পর তিনি আবার হাতে চালিয়েও দেখেননি।
হাতে চালিয়ে দেখার দুটি দুর্বলতা আছে। প্রথমত, আমরা সাধারণত সেই ইনপুটই দিই, যেটা কাজ করবে বলে মনে হয়। দ্বিতীয়ত, হাতের পরীক্ষা কোথাও লেখা থাকে না — পরের বার কেউ আর চালায় না। Test এই দুটিই সারায়। একটি test হলো একটি লিখিত প্রতিশ্রুতি: “grade(80) সবসময় 'A' দেবে।” প্রতিশ্রুতিটি কোডে লেখা থাকে, তাই কম্পিউটার যতবার খুশি, এক সেকেন্ডেরও কম সময়ে, ক্লান্তি ছাড়াই যাচাই করতে পারে।
Test বলে “এখানে আর কখনো ভুল হবে না — হলে আমি জানাব।”
Test লেখার চারটি আসল লাভ
🛡️ আত্মবিশ্বাস
“মনে হয় ঠিক আছে” নয় — “এই ৪০টি পরিস্থিতিতে ঠিক আছে, প্রমাণসহ।”
🔧 নির্ভয়ে বদলানো
Test থাকলে refactoring করতে ভয় লাগে না — কোড পরিষ্কার করুন, test চালান, সবুজ হলে আচরণ বদলায়নি।
🔁 পুরোনো বাগ ফেরে না
এক জায়গায় বদল আরেক জায়গা ভেঙে দিলে তাকে বলে regression. প্রতিবার সব test চালালে সেটি সাথে সাথে ধরা পড়ে।
📖 জীবন্ত দলিল
Test পড়লেই বোঝা যায় ফাংশনটি শূন্যে বা ঋণাত্মকে কী করে। পুরোনো হলে test নিজেই ফেল করে জানায়।
একটি গার্মেন্টস কারখানার কথা ভাবুন। কাটিং টেবিলে ভুল মাপ ধরা পড়লে নষ্ট হয় এক টুকরো কাপড়। সেলাই লাইনের শেষে ধরা পড়লে নষ্ট হয় পুরো শার্ট। আর ভুলটা বিদেশি ক্রেতার গুদামে ধরা পড়লে — পুরো কনটেইনার ফেরত, জরিমানা, পরের অর্ডার হারানোর ভয়। সফটওয়্যারেও একই নিয়ম: বাগ যত দেরিতে ধরা পড়ে, তার দাম তত বেশি। তাই “কোয়ালিটি চেক” বসাতে হয় প্রতিটি ধাপে।
AI-এর যুগে কেন test আরও জরুরি? আগে আপনি যে কোড চালাতেন, তার প্রতিটি লাইন আপনিই লিখতেন। এখন অনেক কোড আসে AI থেকে — দ্রুত, আত্মবিশ্বাসী ভঙ্গিতে, আর মাঝে মাঝে সূক্ষ্ম ভুলসহ। যে কোড আপনি লেখেননি, তা যাচাইয়ের সবচেয়ে নির্ভরযোগ্য উপায় হলো আপনার নিজের লেখা test. আপনার spec থেকে বানানো test-ই ঠিক করে দেয় কোডটি গ্রহণ করবেন কি না — কে লিখেছে তাতে কিছু যায় আসে না।
২ · টেস্টিংয়ের ভাষা — unit, integration, end-to-end
প্রতিটি test-এর ভেতরে একই কাঠামো: একটি ইনপুট, আর তার জন্য প্রত্যাশিত ফল। এই জোড়াটিকে বলে test case. প্রত্যাশিত ফলটি আসতে হবে spec থেকে — কোড কী করছে তা দেখে নয়, কোডের কী করার কথা তা থেকে। অনেকগুলো test case মিলে হয় একটি test suite.
Test কতটা বড় অংশ একসাথে পরীক্ষা করছে, সেই অনুযায়ী তিনটি স্তর:
| স্তর | কী পরীক্ষা করে | গতি ও সংখ্যা | দোকানের খাতা প্রজেক্টে উদাহরণ (পর্ব ২৯) |
|---|---|---|---|
| Unit test | একটি ফাংশন বা ছোট মডিউল, একা | মিলিসেকেন্ডে, শত শত | ledger_total() খালি খাতায় 0 দেয় কি না |
| Integration test | কয়েকটি মডিউল একসাথে — জোড়াগুলো ঠিক আছে কি না | সেকেন্ডে, কয়েক ডজন | খাতায় লিখে ফাইলে সেভ, আবার খুলে পড়লে একই হিসাব |
| End-to-end test | পুরো প্রোগ্রাম, ব্যবহারকারীর চোখে | ধীর, অল্প কয়েকটি | ./shop add 500 চালিয়ে আউটপুট আর exit code মেলানো |
এই তিনটির অনুপাত নিয়ে একটি জনপ্রিয় ছবি আছে — test pyramid. নিচে চওড়া ভিত্তি: অনেক দ্রুত unit test. মাঝে কিছু integration test. চূড়ায় অল্প কয়েকটি end-to-end test. কারণটা সহজ: unit test দ্রুত চলে, আর ফেল করলে ঠিক কোন ফাংশনে সমস্যা তা সাথে সাথে বলে দেয়। End-to-end test বাস্তবের কাছাকাছি, কিন্তু ধীর, আর ফেল করলে শুধু বলে “কোথাও কিছু ভুল।” ধারণাটি জনপ্রিয় করেন Mike Cohn, ২০০৯ সালের একটি বইয়ে।
Test pyramid
Many fast unit tests, fewer integration tests, a handful of end-to-end tests
- Unit
~৭০%দ্রুত, নির্দিষ্ট, সস্তা - Integration
~২০%মডিউলের জোড়া - End-to-end
~১০%পুরো প্রোগ্রাম, ধীর
একটি ভালো test-এর তিন ভাগ — Arrange, Act, Assert
যেকোনো test পড়তে সহজ হয়, যদি সেটি তিন ভাগে সাজানো থাকে। একে বলে Arrange-Act-Assert: আগে পরিস্থিতি সাজান, তারপর একটি কাজ করুন, শেষে ফল যাচাই করুন।
void test_grade_at_80_is_A(void) {
int marks = 80; /* Arrange: ইনপুট ও পরিস্থিতি সাজানো */
char g = grade(marks); /* Act: ঠিক একটি কাজ */
CHECK(g == 'A'); /* Assert: প্রত্যাশিত ফল যাচাই */
}নাম পড়েই বোঝা যায় কোন প্রতিশ্রুতি ভাঙলে এটি ফেল করবে। একটি test-এ একটিই আচরণ যাচাই করুন — তাহলে ফেল করলে কারণ খুঁজতে হয় না।
৩ · কোন ইনপুটে টেস্ট করবেন — equivalence class ও boundary
একটি int প্রায় ৪৩০ কোটি রকম মান নিতে পারে। সবগুলো দিয়ে test করা অসম্ভব, আর দরকারও নেই। কৌশলটা হলো ইনপুটকে এমন দলে ভাগ করা, যেখানে একই দলের সবাই একই রকম আচরণ পায়। এই দলকে বলে equivalence class. একই দলের ৫০ আর ৫১ দিয়ে আলাদা test লিখে নতুন কিছু জানা যায় না।
কিন্তু বাগ সবচেয়ে বেশি লুকিয়ে থাকে দলের সীমানায় — যেখানে < আর <=-এর পার্থক্য গুরুত্বপূর্ণ। তাই প্রতিটি সীমানার দুই পাশের মান, অর্থাৎ boundary value, আলাদা করে পরীক্ষা করতে হয়। আমাদের উদাহরণের spec একটি সাধারণ গ্রেডিং নিয়ম (কোনো নির্দিষ্ট বোর্ডের নয়):
নম্বর m হবে ০ থেকে ১০০-র মধ্যে। ৮০–১০০ হলে 'A', ৩৩–৭৯ হলে 'P' (পাস), ০–৩২ হলে 'F' (ফেল)। সীমার বাইরের যেকোনো সংখ্যায় 'X' (অবৈধ)।
নিচের ডায়াগ্রামে দেখুন, এই spec থেকে কীভাবে ধাপে ধাপে test case বাছাই করা হয়:
- Spec থেকে শুরু। কোড দেখার আগেই spec পড়ে সংখ্যারেখাটি পাঁচ ভাগে ভাগ করা যায়: শূন্যের নিচে, ফেল, পাস, A, আর একশোর ওপরে। প্রত্যাশিত ফল আসছে spec থেকে, কোড থেকে নয় — এটাই ঠিক পথ।
- পাঁচটি equivalence class. একই দলের ৪০ আর ৬০ দুটোই
'P', তাই প্রতিটি দল থেকে একটি প্রতিনিধিই যথেষ্ট। খেয়াল করুন, দুটি “অবৈধ” দলও আছে — ভুল ইনপুট পরীক্ষা না করা নতুনদের সবচেয়ে সাধারণ ভুল। - নিচের সীমানা: -1 আর 0. ঠিক ০ বৈধ, ঠিক -১ অবৈধ। শর্তে
m <= 0লিখে ফেললে ০ অবৈধ হয়ে যাবে — শুধু এই জোড়াটিই সেই ভুল ধরতে পারে, মাঝের ১৫ কখনো নয়। - ফেল/পাস সীমানা: 32 আর 33.
m > 33লিখলে ঠিক ৩৩ পাওয়া শিক্ষার্থী ফেল দেখবে। দুই পাশের দুটি মানই রাখুন। - পাস/A সীমানা: 79 আর 80. নাদিয়ার বাগটি ঠিক এখানে ছিল। ৫০ আর ৯০ সীমানা থেকে দূরে, তাই দুটোই পাস করে; ৮০ দিলেই বাগ ধরা পড়ে।
- ওপরের সীমানা: 100 আর 101. ১০০ বৈধ সর্বোচ্চ মান; ১০১ অবৈধ।
m >= 100বনামm > 100— আবারও এক অক্ষরের পার্থক্য। - চূড়ান্ত তালিকা। ৮টি boundary মান প্রতিটি দলকেও ছুঁয়ে যায়; চাইলে একটি মাঝের মান যোগ করুন। আর “abc” টাইপ করলে? সেটি
grade()-এর কাজ নয় — ইনপুট পড়ার স্তর সামলাবে, তার test আলাদা।
তিন ধরনের পরিস্থিতি — আর একটি পাঁচ শব্দের চেকলিস্ট
Test case ভাবার সময় তিনটি ঝুড়ি মাথায় রাখুন। Happy path: স্বাভাবিক, প্রত্যাশিত ইনপুট (নম্বর ৬৫)। Edge case: সীমানা বা অস্বাভাবিক কিন্তু বৈধ ইনপুট (ঠিক ০, ঠিক ১০০, খালি তালিকা)। Error case: অবৈধ ইনপুট, যেখানে প্রোগ্রামের কাজ ঠিকভাবে “না” বলা (-১, NULL pointer, না খোলা ফাইল)। AI এবং নতুন প্রোগ্রামার দুজনেই প্রথম ঝুড়িতে থেমে যান।
আর যেকোনো ফাংশনের জন্য এই পাঁচ শব্দের চেকলিস্টটি প্রায় জাদুর মতো কাজ করে:
| শব্দ | প্রশ্ন | উদাহরণ: অ্যারের গড় বের করার ফাংশন |
|---|---|---|
| Zero (শূন্য) | কিছুই না থাকলে? | n = 0 — শূন্য দিয়ে ভাগ হবে কি? |
| One (এক) | ঠিক একটি থাকলে? | {70} → গড় 70 |
| Many (অনেক) | সাধারণ অবস্থা | {60, 70, 80} → 70 |
| Max (সর্বোচ্চ) | সীমার একদম ধারে? | অনেক বড় মান — যোগফল int overflow করবে কি? |
| Invalid (অবৈধ) | নিয়ম ভাঙা ইনপুট? | arr == NULL, n < 0 |
এখানে যে কৌশলগুলো দেখলেন, সেগুলো কোডের ভেতর না দেখে শুধু spec থেকে তৈরি — একে বলে black-box testing. কোডের প্রতিটি শাখা দেখে test বানানো হলো white-box testing (অংশ ৭-এ coverage দেখার সময় আসবে)। পর্ব ১১-এ দেখা decision table-ও একই পরিবারের হাতিয়ার — অনেকগুলো শর্ত একসাথে থাকলে সব মিশ্রণ সারি ধরে লিখে প্রতিটি সারিকে একটি test বানানো যায়।
৪ · নিজের হাতে test harness — CHECK macro
Test চালানোর জন্য কোনো বড় library লাগে না। C-তে মাত্র তিনটি জিনিস দিয়েই একটি কাজের test harness বানানো যায়: শর্ত যাচাইয়ের একটি macro, পাস ও ফেলের দুটি গণনা, আর শেষে ফেল থাকলে শূন্য নয় এমন exit status. পর্ব ২৭-এ শেখা preprocessor-এর তিনটি কৌশল এখানে একসাথে কাজে লাগে।
কেন ফাংশন নয়, macro? ফেল হলে আমরা জানতে চাই কোন লাইনে আর কোন শর্তটি। ফাংশন শুধু 0 বা 1 পায়, শর্তের লেখাটি দেখে না; macro দেখে, কারণ preprocessor লেখার স্তরে কাজ করে। নিচে দেখুন একটি CHECK লাইন আসলে কী হয়ে যায়:
- আপনার লাইন
CHECK(...)লাইন ১৭ - Preprocessor
gcc -Eলেখা বদলে বসায় - চলার সময়
FAIL ...:17ঠিক ঠিকানাসহ রিপোর্ট
#define CHECK(cond) do { if (cond) passed++; else { failed++; \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0)
...
CHECK(grade(80) == 'A');
do { if (grade(80) == 'A') passed++; else { failed++;
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0);
do { if (grade(80) == 'A') passed++; else { failed++;
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, "grade(80) == 'A'"); } } while (0);
do { if (grade(80) == 'A') passed++; else { failed++; printf("FAIL %s:%d: %s\n", "grade_test.c", 17, "grade(80) == 'A'"); } } while (0);
if (quick_mode)
CHECK(grade(80) == 'A'); /* ; শেষে থাকলেও সমস্যা নেই */
else
CHECK(grade(100) == 'A');
/* শুধু { ... } দিয়ে লিখলে "};" হয়ে যেত — else একা পড়ে compile error */
$ ./grade_test
FAIL grade_test.c:17: grade(80) == 'A'
7 passed, 1 failed
$ echo $?
1
- আপনি একটি লাইন লিখলেন। দেখতে ফাংশন ডাকার মতো, কিন্তু এটি একটি function-like macro. Compiler এই লাইন কখনো দেখবে না; তার আগেই preprocessor এটিকে বদলে দেবে। সংজ্ঞার শেষের
\মানে “পরের লাইনেও সংজ্ঞা চলছে।” - শর্তটি বসানো হলো। প্রতিটি
cond-এর জায়গায় হুবহু বসে গেলgrade(80) == 'A'. এটি শুধু লেখা বদলানো (macro expansion), কোনো হিসাব নয়। বোঝার সুবিধার জন্য ধাপগুলো আলাদা দেখাচ্ছি; আসলে সব একসাথেই হয়। #condলেখা হয়ে গেল। Macro-র ভেতরে#দিলে argument-টি string literal হয়ে যায় — একে বলে stringize. তাই ফেল হলে প্রোগ্রাম আপনার লেখা শর্তটিই ছাপতে পারে, যা কোনো ফাংশন পারে না।- ঠিকানা বসানো হলো।
__FILE__আর__LINE__দুটি predefined macro — preprocessor নিজেই বসিয়ে দেয় ফাইলের নাম আর যেখানে CHECK লেখা হয়েছে সেই লাইনের নম্বর। পুরো জিনিসটি এখন একটিমাত্র লম্বা লাইন।gcc -E grade_test.cচালালে ঠিক এটাই দেখবেন। do { … } while (0)কেন? এটি এমন একটি লুপ যা ঠিক একবার চলে — উদ্দেশ্য শুধু পুরো macro-কে একটি statement বানানো। তাহলেif-else-এর ভেতরে, শেষে;দিয়েও নিরাপদে লেখা যায়। শুধু{ … }দিয়ে লিখলে};হয়েelseএকা পড়ে যেত।- চলার সময় রিপোর্ট। ধরুন
grade()-এ ভুল করেm > 80লেখা। তখন ঠিক এই লাইনটি ছাপা হবে: ফাইল, লাইন ১৭, আর ভাঙা প্রতিশ্রুতিটি। বাকি test-গুলো চলতে থাকে, শেষে সারাংশ আসে, আর exit code হয় 1 — ফলেmake testবা CI বুঝে যায় কিছু ভেঙেছে।
এবার পুরো জিনিসটি একসাথে চালিয়ে দেখুন। Spec অনুযায়ী লেখা grade() আর অংশ ৩-এর ৮টি boundary test:
#include <stdio.h>
static int passed = 0, failed = 0;
#define CHECK(cond) do { if (cond) passed++; else { failed++; \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0)
char grade(int m) {
if (m < 0 || m > 100) return 'X';
if (m >= 80) return 'A';
if (m >= 33) return 'P';
return 'F';
}
int main(void) {
CHECK(grade(0) == 'F'); CHECK(grade(32) == 'F');
CHECK(grade(33) == 'P'); CHECK(grade(79) == 'P');
CHECK(grade(80) == 'A'); CHECK(grade(100) == 'A');
CHECK(grade(-1) == 'X'); CHECK(grade(101) == 'X');
printf("%d passed, %d failed\n", passed, failed);
return failed != 0;
}সব ঠিক থাকলে আউটপুট একটিই লাইন: 8 passed, 0 failed. পাস করা test চুপ থাকে — শুধু ফেল করা test কথা বলে, যাতে আসল সমস্যা ভিড়ে হারিয়ে না যায়। এবার নিজে একটি বাগ ঢুকিয়ে দেখুন: লাইন ৯-এ >= বদলে > করুন। ঠিক একটি FAIL লাইন আসবে, তাতে ফাইলের নাম আর লাইন ১৭ (যেখানে grade(80) যাচাই হচ্ছে)। শেষ লাইন return failed != 0; — ফেল থাকলে 1, না থাকলে 0. এই ছোট্ট সংখ্যাটির ওপরেই পুরো অটোমেশন দাঁড়িয়ে আছে (অংশ ৮)।
assert আর CHECK — একই রকম দেখতে, কাজ আলাদা
পর্ব ২৭ ও ৩০-এ আপনি assert দেখেছেন। দুটিই একটি শর্ত যাচাই করে, কিন্তু উদ্দেশ্য সম্পূর্ণ ভিন্ন:
| বিষয় | assert(cond) | Test macro CHECK(cond) |
|---|---|---|
| কোথায় থাকে | আসল প্রোগ্রামের কোডে | শুধু test ফাইলে |
| কী ধরে | প্রোগ্রামারের ভুল ধারণা — “এখানে pointer কখনো NULL হবে না” | ফাংশনের আচরণ spec-এর সাথে মেলে কি না |
| ফেল করলে | সাথে সাথে abort() — প্রোগ্রাম শেষ | গুনে রাখে, পরের test চালিয়ে যায় |
| Release build-এ | NDEBUG দিলে পুরোপুরি মুছে যায় | কখনো মোছে না — test build আলাদাই থাকে |
Test-এ শুধু assert ব্যবহার করলে দুটি সমস্যা। প্রথম ফেলেই সব থেমে যায় — বাকি ৩৯টি test-এর খবর আর পাবেন না। আর কেউ -DNDEBUG দিয়ে build করলে সব test নিঃশব্দে উধাও হয়ে যায়, অথচ “সব পাস” দেখায়! এজন্যই test-এর জন্য আলাদা macro.
৫ · Test করা যায় এমন কোড — হিসাব আর I/O আলাদা
কিছু কোড test করা খুব সহজ, কিছু প্রায় অসম্ভব। পার্থক্যটা কোডের নকশায়। নিচের ফাংশনটি দেখুন:
/* test করা কঠিন: পড়া, হিসাব, ছাপা — সব এক জায়গায় */
void print_grade(void) {
int m;
scanf("%d", &m);
if (m >= 80) printf("A\n");
else if (m >= 33) printf("P\n");
else printf("F\n");
}এটিকে test করতে হলে কাউকে কীবোর্ডে বসে নম্বর টাইপ করতে হবে, তারপর স্ক্রিন পড়তে হবে। হিসাবের নিয়মটি I/O-র ভেতরে বন্দি। সমাধান হলো আলাদা করা: হিসাব থাকবে একটি pure function-এ — একই ইনপুটে সবসময় একই ফল, কোনো side effect নেই — আর পড়া-ছাপা থাকবে বাইরের একটি পাতলা স্তরে। তখন grade()-কে একশোবার ডেকে test করা যায়, কীবোর্ড ছাড়াই। পর্ব ২৯-এ দোকানের খাতা মডিউলে কোনো printf বা scanf রাখেননি — সেটি এই কারণেই।
ভেতরের অংশ যত বড় ও খাঁটি, test তত সহজ।
যা বদলায়, তা বাইরে থেকে দিন — dependency injection-এর সহজ রূপ
কিছু ফাংশন এমন জিনিসের ওপর নির্ভর করে, যা প্রতিবার বদলায়: বর্তমান সময়, random সংখ্যা, ফাইল, নেটওয়ার্ক। ধরুন একটি bKash-ধাঁচের OTP দুই মিনিট পর বাতিল হয়। ফাংশনটি ভেতরে নিজেই time(NULL) ডাকলে test করতে আপনাকে সত্যিই দুই মিনিট অপেক্ষা করতে হবে! উপায় হলো নির্ভরতাটি বাইরে থেকে পাঠানো — একে বলে dependency injection. C-তে এর সবচেয়ে সহজ রূপ হলো পর্ব ২৮-এ শেখা function pointer বা callback:
typedef long (*clock_fn)(void); /* "এখন কত সেকেন্ড" জানার ফাংশন */
int otp_valid(long issued_at, clock_fn now) {
return now() - issued_at <= 120; /* ১২০ সেকেন্ড পর্যন্ত বৈধ */
}
/* আসল প্রোগ্রামে: otp_valid(t, real_clock) — real_clock ভেতরে time() ডাকে */
/* test-এ: নকল ঘড়ি, যার সময় আমরাই ঠিক করি */
static long fake_t;
static long fake_clock(void) { return fake_t; }
/* fake_t = 1120; CHECK(otp_valid(1000, fake_clock) == 1); ঠিক ১২০ সেকেন্ড */
/* fake_t = 1121; CHECK(otp_valid(1000, fake_clock) == 0); এক সেকেন্ড বেশি */এই নকল ঘড়ির মতো আসল জিনিসের বিকল্পকে বলে test double — কখনো mock, কখনো stub বা fake নামে ডাকা হয়। একই কৌশল ফাইলের জন্যও: ফাংশন নিজে fopen না করে একটি FILE * নেবে। আসল প্রোগ্রাম পাঠাবে stdin বা সত্যিকারের ফাইল; test পাঠাবে tmpfile() দিয়ে বানানো একটি অস্থায়ী ফাইল, যাতে আগেই নির্দিষ্ট লেখা রাখা আছে।
পুরো CLI প্রোগ্রাম test করা — run(argc, argv)
পর্ব ২৫-এর কমান্ড-লাইন টুলগুলো কীভাবে test করবেন? কৌশলটা হলো main-কে প্রায় খালি রাখা, আর আসল কাজ একটি run ফাংশনে সরিয়ে নেওয়া:
int run(int argc, char **argv, FILE *out, FILE *err); /* সব কাজ এখানে */
int main(int argc, char **argv) {
return run(argc, argv, stdout, stderr); /* main শুধু জোড়া দেয় */
}
/* test: char *args[] = {"shop", "add", "-5", NULL};
CHECK(run(3, args, out, err) == 2); ঋণাত্মক টাকা → usage error */এখন test নিজেই নকল argv বানিয়ে run ডাকে, আউটপুট অস্থায়ী ফাইলে ধরে মেলায়, আর exit code যাচাই করে — নতুন প্রসেস চালানোর ঝামেলা ছাড়াই।
৬ · TDD — আগে test, তারপর কোড
সাধারণত আমরা আগে কোড লিখি, তারপর (যদি সময় থাকে) test. Test-driven development (TDD) এই ক্রম উল্টে দেয়। তিন ধাপের একটি ছোট চক্র, বারবার:
🔴 Red
পরের ছোট আচরণটির জন্য একটি test লিখুন। চালান — এটি ফেল করতে হবে। ফেল না করলে test কিছুই যাচাই করছে না।
🟢 Green
Test পাস করানোর জন্য সবচেয়ে কম কোড লিখুন — সুন্দর হওয়ার দরকার নেই।
🔵 Refactor
এবার কোড পরিষ্কার করুন। প্রতিটি ছোট বদলের পর test চালান — সবুজ থাকতেই হবে।
“আগে test লেখা” ধারণাটি নতুন নয় — ১৯৬০-এর দশকেই NASA-র কিছু প্রজেক্টে এমন চর্চার কথা জানা যায়। তবে একে একটি নিয়মিত পদ্ধতি হিসেবে নাম দিয়ে জনপ্রিয় করেন Kent Beck, ১৯৯০-এর দশকের শেষ দিকে Extreme Programming আন্দোলনের অংশ হিসেবে, আর ২০০২ সালে তাঁর বই Test-Driven Development: By Example-এ।
একটি পরিচিত উদাহরণে চক্রটি দেখুন: অধিবর্ষ বা leap year যাচাই। নিয়ম — ৪ দিয়ে বিভাজ্য হলে অধিবর্ষ, কিন্তু ১০০ দিয়ে বিভাজ্য হলে নয়, তবে ৪০০ দিয়ে বিভাজ্য হলে আবার অধিবর্ষ। তাই ২০২৪ অধিবর্ষ, ১৯০০ নয়, ২০০০ অধিবর্ষ।
int is_leap(int y) { (void)y; return 0; }
FAIL 2024: want 1, got 0
1/2 tests passed
int is_leap(int y) { return y % 4 == 0; }
2/2 tests passed
int is_leap(int y) { return y % 4 == 0; }
FAIL 1900: want 0, got 1
2/3 tests passed
int is_leap(int y) { return y % 4 == 0 && y % 100 != 0; }
3/3 tests passed
int is_leap(int y) { return y % 4 == 0 && y % 100 != 0; }
FAIL 2000: want 1, got 0
3/4 tests passed
int is_leap(int y) {
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}
4/4 tests passed
int is_leap(int y) {
if (y % 400 == 0) return 1; /* ২০০০ */
if (y % 100 == 0) return 0; /* ১৯০০ */
return y % 4 == 0; /* ২০২৪, ২০২৩ */
}
4/4 tests passed
- Red — প্রথম test. দুটি সাধারণ বছর দিয়ে শুরু: ২০২৪ অধিবর্ষ, ২০২৩ নয়। ফাংশনটি এখন শুধু একটি খালি কাঠামো, সবসময় 0 দেয়। ২০২৪-এর test ফেল করল — ভালো খবর! এতে প্রমাণ হলো test-টি সত্যিই কিছু যাচাই করছে। (
(void)y;শুধু “unused parameter” warning থামায়।) - Green — সবচেয়ে সরল সমাধান। “৪ দিয়ে বিভাজ্য” — এক লাইন। দুটি test-ই পাস। আমরা জানি এটি অসম্পূর্ণ, কিন্তু TDD-এর নিয়ম হলো: যে আচরণের জন্য এখনো test নেই, তার কোডও এখনো নয়।
- Red — ১৯০০ যোগ করলাম। ১৯০০ সাল ৪ দিয়ে বিভাজ্য, কিন্তু অধিবর্ষ নয়। কোড না ছুঁয়ে শুধু একটি নতুন প্রতিশ্রুতি লিখলাম — আর সেটি ভাঙা দেখাচ্ছে।
- Green — ১০০-র নিয়ম।
&& y % 100 != 0যোগ হলো। তিনটিই পাস। এই অবস্থার কোডটিই নিচের দ্বিতীয় উদাহরণে চালাবেন। - Red — ২০০০ যোগ করলাম। ২০০০ সাল ১০০ দিয়ে বিভাজ্য, কিন্তু ৪০০ দিয়েও — তাই অধিবর্ষ। আমাদের কোড বলছে “না।”
- Green — ৪০০-র নিয়ম।
|| y % 400 == 0যোগ হলো। চারটিই সবুজ। কোড সঠিক, কিন্তু এক লাইনে তিনটি নিয়ম — পড়তে কষ্ট। - Refactor — পড়তে সহজ করা। নিয়মগুলো আলাদা লাইনে সাজালাম: আগে সবচেয়ে বিশেষ নিয়ম (৪০০), শেষে সাধারণটি (৪)। আচরণ বদলায়নি — মাথা খাটিয়ে নয়, test চালিয়ে জানলাম। এটাই TDD-এর আসল উপহার।
নিচের প্রোগ্রামে stepper-এর ধাপ ৪-এর কোডটি রাখা হয়েছে, কিন্তু এবার পুরো test table-সহ — একটি টেবিলে বছর আর প্রত্যাশিত ফল, আর একটি লুপ সবগুলো যাচাই করে। একে বলে table-driven test. চালিয়ে দেখুন কোনটি ফেল করে:
#include <stdio.h>
int is_leap(int y) { /* BUG on purpose: the 400 rule is missing */
return y % 4 == 0 && y % 100 != 0;
}
int main(void) {
struct { int year, want; } t[] = {
{2024, 1}, {2023, 0}, {1900, 0}, {2000, 1}, {2100, 0}
};
int n = sizeof t / sizeof t[0], failed = 0;
for (int i = 0; i < n; i++) {
int got = is_leap(t[i].year);
if (got != t[i].want) {
printf("FAIL year %d: want %d, got %d\n", t[i].year, t[i].want, got);
failed++;
}
}
printf("%d/%d tests passed\n", n - failed, n);
return failed != 0;
}আউটপুট: FAIL year 2000: want 1, got 0, তারপর 4/5 tests passed. প্রোগ্রামটি ইচ্ছা করেই 1 exit code দিয়ে শেষ হয় — test ফেল করলে এটাই হওয়ার কথা। লক্ষ করুন, এই ফেল মেসেজটি শুধু “ভুল” বলে না; বলে কোন ইনপুটে, কী চাওয়া হয়েছিল, কী পাওয়া গেল। ভালো test-এর রিপোর্ট পড়েই কারণ আন্দাজ করা যায়: ২০০০ সালকে “না” বলছে, মানে ৪০০-র নিয়ম নেই। Table-driven ধরনের সুবিধা হলো নতুন test যোগ করতে শুধু টেবিলে একটি সারি বাড়াতে হয় — যেমন {1600, 1}. ঠিক করতে || y % 400 == 0 যোগ করুন, আবার চালান — 5/5.
প্রতিটি বাগের জন্য একটি regression test
পর্ব ৩১-এ debugger দিয়ে বাগ খুঁজে ঠিক করেছেন। কিন্তু পেশাদার কাজ সেখানে শেষ নয়। নিয়মটি হলো: ঠিক করার আগে এমন একটি test লিখুন যা বাগটির কারণে ফেল করে, তারপর কোড ঠিক করুন, তারপর test-টি চিরদিনের জন্য suite-এ রেখে দিন। একে বলে regression test. ছয় মাস পর কেউ — আপনি, সহকর্মী বা AI — একই ভুল আবার করলে সাথে সাথে লাল হবে।
৭ · উদাহরণের বাইরে — property, fuzzing, stress আর coverage
এতক্ষণের সব test ছিল উদাহরণ-ভিত্তিক: “এই ইনপুটে এই ফল।” এর একটি মৌলিক সীমা আছে — আপনি যে ইনপুটের কথা ভাবেননি, তার test-ও লেখেননি। আর বাগ লুকিয়ে থাকে ঠিক সেখানেই। নিচের কৌশলগুলো যন্ত্রকে দিয়ে নতুন ইনপুট বানিয়ে নেয়।
Property-based testing — নির্দিষ্ট উত্তর নয়, নিয়ম
অনেক সময় প্রতিটি ইনপুটের সঠিক উত্তর হাতে জানা থাকে না, কিন্তু উত্তরটি কোন নিয়ম মানবে তা জানা থাকে। Property-based testing-এ আপনি নিয়মটি লেখেন, আর যন্ত্র হাজার হাজার এলোমেলো ইনপুট বানিয়ে যাচাই করে। কয়েকটি উদাহরণ:
is_leap(y) == is_leap(y + 400)— যেকোনো বছরের জন্য, কারণ গ্রেগরিয়ান ক্যালেন্ডার প্রতি ৪০০ বছরে পুনরাবৃত্তি হয়।- বৈধ
a <= bহলেgrade(a)কখনোgrade(b)-এর চেয়ে ভালো হবে না — বেশি নম্বরে খারাপ গ্রেড অসম্ভব। - যেকোনো sort ফাংশনের ফল সাজানো থাকবে, আর তাতে ঠিক আগের উপাদানগুলোই থাকবে — একটিও হারাবে না, নতুন আসবে না।
ধারণাটি জনপ্রিয় করে Haskell ভাষার QuickCheck (২০০০ সালের দিকে)।
Fuzzing — যন্ত্রকে দিয়ে আবর্জনা ইনপুট খাওয়ানো
Fuzzing হলো প্রোগ্রামকে লক্ষ লক্ষ অদ্ভুত, ভাঙাচোরা, অপ্রত্যাশিত ইনপুট দেওয়া, আর দেখা কোনটিতে সে crash করে বা sanitizer অভিযোগ করে। এখানে “সঠিক উত্তর” জানার দরকারই নেই — প্রশ্ন একটিই: প্রোগ্রাম কি ভেঙে পড়ল? আধুনিক fuzzer যেমন AFL আর libFuzzer অন্ধভাবে এলোমেলো নয়; তারা দেখে কোন ইনপুট কোডের নতুন কোনো পথ খুলল, আর সেই ইনপুট থেকে আরও নতুন ইনপুট বানায়। নিচে দেখুন একটি fuzzer কীভাবে একটি লুকানো buffer overflow খুঁজে পায়:
- Seed
corpus/ভালো ইনপুট - Mutate
bit flip…একটু বদল - নতুন পথ?
coverageরাখো বা ফেলো - Crash
ASanভুল ধরা পড়ল - Test
CHECKচিরস্থায়ী পাহারা
Rafi:80
Mitu:95
/* ভেতরে: char name[16]; strcpy(r->name, ...); — বাগটি এখানে লুকানো */
Rafi:8 Raf:80 Rafi::80 :80 RafiRafi:80 R\xff\x00fi:80
#1024 NEW cov: 31 "RafiRafi:80" <- লম্বা নাম, নতুন পথ #5120 NEW cov: 33 "RafiRafiRafi:80" #9000 --- cov: 33 "Rafi:8" <- নতুন কিছু নেই, বাদ
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 23 ... in strcpy
#1 in parse_record record.c:9
input: "RafiRafiRafiRafiRafiRa:80"
AAAAAAAAAAAAAAAA:1 <- ঠিক ১৬ অক্ষর: NUL-এর জায়গা নেই
/* regression: fuzzer crash, 16-char name */ CHECK(parse_record("AAAAAAAAAAAAAAAA:1", &r) == -1); CHECK(parse_record("AAAAAAAAAAAAAAA:1", &r) == 0); /* ১৫ — চলবে */
- Seed — শুরুর ইনপুট। আপনি fuzzer-কে কয়েকটি স্বাভাবিক উদাহরণ দেন, যাকে বলে seed corpus. আমাদের
parse_record()“নাম:নম্বর” পড়ে একটি struct-এ রাখে। ভেতরেchar name[16]আর দৈর্ঘ্য না মেপেstrcpy— কিন্তু সব unit test-এ নাম ছোট ছিল, তাই কেউ টের পায়নি। - Mutate — একটু একটু বদল। Fuzzer ইনপুটের অক্ষর উল্টায়, মুছে দেয়, কপি করে লম্বা করে, অদ্ভুত বাইট ঢোকায় — প্রতি সেকেন্ডে হাজার হাজার।
- নতুন পথ খুললে রাখো। Fuzzer দেখে প্রতিটি ইনপুট কোডের কোন শাখা চালাল। যে ইনপুট নতুন শাখায় পৌঁছায়, সেটি corpus-এ রেখে তা থেকে আরও বদলায় — এভাবে ধীরে ধীরে “লম্বা নাম”-এর মতো গভীর কোণে পৌঁছে যায়।
- Crash! এক সময় নাম ১৬ অক্ষর ছাড়াল।
strcpyঅ্যারের সীমা পেরিয়ে লিখল — একটি buffer overflow. Sanitizer চালু থাকায় ঠিক সেই মুহূর্তেই ধরা পড়ল; না থাকলে হয়তো নিঃশব্দে পাশের মেমরি নষ্ট হতো। - Minimize — সবচেয়ে ছোট রূপ। Fuzzer crash করা ইনপুটটি ছোট করতে করতে সবচেয়ে সরল রূপ খোঁজে: ঠিক ১৬টি অক্ষরের নাম। কারণ পরিষ্কার: ১৬ অক্ষর + শেষের NUL = ১৭ বাইট, অথচ জায়গা ১৬. পর্ব ১৯-এর সেই চেনা off-by-one.
- Regression test. দৈর্ঘ্য মেপে ঠিক করার পর ইনপুটটি একটি স্থায়ী test হয়ে suite-এ গেল — সাথে সীমানার অন্য পাশটিও (১৫ অক্ষর)। Fuzzer-এর আবিষ্কার এখন প্রতিটি
make test-এ পাহারা দেবে। সীমানা যাচাইয়ের অভ্যাস (অংশ ৩) এখানেও ফিরে এলো।
libFuzzer-এ একটি fuzz target লেখা খুবই সহজ — একটি ফাংশন, যা বাইটের একটি টুকরো নিয়ে আপনার কোডকে খাওয়ায়:
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char line[64];
if (size >= sizeof line) return 0;
memcpy(line, data, size);
line[size] = '\0'; /* fuzzer-এর বাইটকে string বানানো */
struct record r;
parse_record(line, &r); /* crash হলে fuzzer থেমে ইনপুট সেভ করবে */
return 0;
}clang -g -fsanitize=fuzzer,address fuzz_record.c record.c -o fuzz_record
./fuzz_record corpus/“Fuzz” শব্দটির জন্ম ১৯৮৮ সালে। University of Wisconsin-এর অধ্যাপক Barton Miller ঝড়ের রাতে ফোন-লাইনে লগইন করে দেখেন, লাইনের শব্দে আসা আবর্জনা অক্ষরে Unix প্রোগ্রাম crash করছে। ছাত্রদের দিয়ে পরীক্ষা করিয়ে দেখা গেল, সাধারণ Unix টুলগুলোর উল্লেখযোগ্য একটি অংশ এলোমেলো ইনপুটে crash করে বা আটকে যায়। পরে Michał Zalewski-র AFL (২০১৩–১৪) coverage-নির্দেশিত fuzzing জনপ্রিয় করে, আর ২০১৬ সালে Google শুরু করে OSS-Fuzz, যা open-source প্রজেক্টে হাজার হাজার বাগ খুঁজে দিয়েছে।
Stress testing — brute force-এর সাথে মিলিয়ে দেখা
Fuzzing প্রশ্ন করে “crash করল কি?” কিন্তু প্রোগ্রাম crash না করেও ভুল উত্তর দিতে পারে। তখন দরকার একটি তুলনা: একটি ধীর কিন্তু নিশ্চিতভাবে সঠিক brute force সমাধান, আর আপনার দ্রুত সমাধান — দুটোকে একই এলোমেলো ইনপুট দিয়ে উত্তর মেলানো। এটি stress testing, যা পর্ব ৪৪-এ হাতে-কলমে করবেন।
Code coverage — test আসলে কোন লাইনগুলো চালাল
Code coverage মাপে আপনার test চলার সময় কোডের কত অংশ অন্তত একবার চলেছে। GCC-র সাথেই আসে gcov:
gcc --coverage -O0 grade_test.c -o grade_test
./grade_test
gcov grade_test.c # grade_test.c.gcov ফাইলে প্রতিটি লাইন কতবার চললধরুন আপনি ০ আর ৩২-এর test দুটি বাদ দিয়েছেন। রিপোর্টে grade()-এর অংশটি এমন দেখাবে — বাঁ পাশের সংখ্যা হলো লাইনটি কতবার চলেছে:
6: 7:char grade(int m) {
6: 8: if (m < 0 || m > 100) return 'X';
4: 9: if (m >= 80) return 'A';
2: 10: if (m >= 33) return 'P';
#####: 11: return 'F';
##### মানে এই লাইন একবারও চলেনি — “ফেল” পথটি কোনো test ছোঁয়নি। Coverage-এর আসল কাজ এটাই: কোথায় test নেই তা দেখানো। একটু সূক্ষ্ম রূপ হলো branch coverage — প্রতিটি if-এর সত্য আর মিথ্যা, দুটি পথই চলেছে কি না।
১০০% coverage মানে কোড সঠিক নয়। Coverage শুধু বলে লাইনটি চলেছে — ফল মেলানো হয়েছে কি না বলে না; কিছু যাচাই না করা grade(50);-ও লাইনগুলো “covered” দেখায়। আর যে কোড লেখাই হয়নি — যেমন ভুলে বাদ পড়া m > 100 যাচাই — তার coverage মাপাই যায় না। Coverage টর্চলাইটের মতো: অন্ধকার কোণ দেখায়, আলো পড়া জায়গা পরিষ্কার কি না বলে না।
৮ · Framework, make test আর CI — test-কে অভ্যাসে পরিণত করা
আমাদের CHECK harness ছোট প্রজেক্টে যথেষ্ট। বড় প্রজেক্টে মানুষ তৈরি test framework ব্যবহার করে — test নিজে খুঁজে চালানো, প্রত্যাশিত আর আসল মান পাশাপাশি দেখানো, crash সামলানো, mock বানানোর সুবিধাসহ।
| Framework | ভাষা | কোথায় জনপ্রিয় / বিশেষত্ব |
|---|---|---|
| Unity | C | খুব ছোট, embedded ও microcontroller-এর কাজে জনপ্রিয় (পর্ব ৫১) |
| CMocka | C | mock ফাংশন বানানোর সুবিধা — নির্ভরতা নকল করতে |
| Check | C | প্রতিটি test আলাদা প্রসেসে চালায়, তাই একটির crash বাকিদের থামায় না |
| Criterion | C | আধুনিক, test নিজে নিজে খুঁজে নেয়, লেখা কম |
| GoogleTest | C++ | C++ জগতে সবচেয়ে প্রচলিত; C কোডও এ দিয়ে test করা হয় |
make test — একটি কমান্ডে সব test
পর্ব ২৯-এর Makefile-এ একটি নতুন target যোগ করুন। তাহলে যে কেউ — নতুন সহকর্মী, আপনি নিজে ছয় মাস পরে, বা CI সার্ভার — শুধু make test লিখেই সব test চালাতে পারবে। Test build-এ sanitizer চালু রাখা ভালো অভ্যাস, যাতে পর্ব ৩১-এর মেমরি-ভুলগুলোও test চলার সময়েই ধরা পড়ে:
CC = gcc
CFLAGS = -Wall -Wextra -g -fsanitize=address,undefined
test: tests/test_grade
./tests/test_grade
tests/test_grade: tests/test_grade.c grade.c grade.h
$(CC) $(CFLAGS) tests/test_grade.c grade.c -o $@
.PHONY: testএখানেই আমাদের return failed != 0; লাইনের গুরুত্ব। Make একটি কমান্ডের exit code শূন্য না পেলে থেমে যায় আর “Error” দেখায়। অর্থাৎ একটি test ফেল করলে make test নিজেই ব্যর্থ হয় — আলাদা করে আউটপুট পড়তে হয় না।
CI — প্রতিবার push করলেই test
সবচেয়ে বড় ধাপ হলো test চালানোর দায়িত্ব মানুষের হাত থেকে সরিয়ে যন্ত্রের হাতে দেওয়া। Continuous Integration (CI)-এ প্রতিবার কেউ git-এ কোড push করলে একটি সার্ভার নিজে থেকেই build করে, সব test চালায়, আর ফেল করলে লাল চিহ্ন দেখায় — কোড মূল শাখায় মেশানোর আগেই। GitHub-এ এর জন্য শুধু একটি ছোট ফাইল লাগে:
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make testএরপর থেকে প্রতিটি পরিবর্তন — আপনার হোক, সহকর্মীর হোক বা AI-এর — একই কঠোর পরীক্ষার মধ্য দিয়ে যাবে, ঝিমিয়ে পড়া বা ভুলে যাওয়ার সুযোগ নেই। পেশাদার দলে একটি সাধারণ নিয়ম: “CI লাল থাকলে কেউ নতুন কিছু merge করবে না।” CI ও deployment-এর পুরো জগৎ দেখতে পারবেন Cloud & DevOps কোর্সে, আর git ও দলগত কাজের নিয়ম Software Engineering & Git কোর্সে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই বিষয়ে প্রায়ই যেসব প্রশ্ন আসে:
- Unit test আর integration test-এর পার্থক্য কী? Unit test একটি ফাংশন বা মডিউল একা পরীক্ষা করে; integration test কয়েকটি অংশ একসাথে জোড়া লাগিয়ে দেখে।
- ১ থেকে ১০০ বৈধ হলে boundary value analysis-এ কোন মানগুলো নেবেন? ০, ১, ১০০, ১০১ — প্রয়োজনে ২ আর ৯৯ও।
- Black-box আর white-box testing কী? প্রথমটি শুধু spec দেখে test বানায়; দ্বিতীয়টি কোডের ভেতরের শাখা দেখে, যেমন coverage মেপে।
- Macro কেন
do { … } while (0)দিয়ে মোড়ানো হয়? যাতে পুরো macro একটি statement হয় এবংif-else-এর ভেতরে;-সহ নিরাপদে বসে। - Regression testing কী, আর কেন জরুরি? পুরোনো test আবার চালিয়ে দেখা যে নতুন পরিবর্তন আগের কোনো সঠিক আচরণ ভাঙেনি।
৯ · AI-এর যুগে টেস্টিং — যে কোড আপনি লেখেননি
AI খুব দ্রুত test লিখতে পারে — বড় সুবিধা। কিন্তু ফাঁদ আছে: “এই ফাংশনের test লিখে দাও” বললে সে দেখে কোড কী করছে, আর সেটাকেই প্রত্যাশিত ফল ধরে। কোডে বাগ থাকলে test-ও সেই বাগকে “সঠিক” বলে মেনে নেয়।
/* ফাঁদ ১: tautological — কখনো ফেল করতে পারে না */
CHECK(grade(85) == grade(85));
/* ফাঁদ ২: implementation-এর নকল — কোডের ভুলও নকল হয়ে আসে */
CHECK(is_leap(2000) == (2000 % 4 == 0 && 2000 % 100 != 0));
/* ফাঁদ ৩: শুধু happy path — সীমানা আর অবৈধ ইনপুট নেই */
CHECK(grade(50) == 'P'); CHECK(grade(90) == 'A');প্রথমটিকে বলে tautological test — সংজ্ঞা অনুযায়ীই সত্য। দ্বিতীয়টি কোডের ভুল সূত্রটিই প্রত্যাশিত ফল হিসেবে কপি করেছে। তৃতীয়টি নাদিয়ার সেই পুরোনো ভুল — সীমানা থেকে দূরের মান। তিনটিই পাস করবে, আর আপনি ভাববেন সব ঠিক।
Test-কেই test করা — mutation testing
Test কতটা শক্ত, মাপার একটি চমৎকার উপায়: কোডে ইচ্ছা করে ছোট বাগ ঢোকান — >=-কে >, &&-কে || — আর দেখুন test ফেল করে কি না। প্রতিটি বদলানো সংস্করণ একটি mutant. Test ফেল করলে mutant “মারা গেল” — ভালো; পাস করলে “বেঁচে গেল” — মানে test এই ভুল ধরতে পারে না। এই পদ্ধতি mutation testing, ধারণাটি ১৯৭০-এর দশকের। m >= 80-কে m > 80 করলে ফাঁদ ৩-এর test বেঁচে যায়, কিন্তু অংশ ৪-এর boundary test সাথে সাথে মেরে ফেলে।
সঠিক ক্রম হলো: আগে spec, তারপর spec থেকে test, সবশেষে কোড। AI-কে test লেখাতে চাইলে কোড নয়, spec দিন — তাহলে test আসবে নিয়ম থেকে, কোডের আচরণ থেকে নয়। নিচের prompt-টি ব্যবহার করে দেখুন:
AI-এর লেখা test হাতে পেলে — যাচাইয়ের চেকলিস্ট
শেষ একটি সতর্কতা: AI মাঝে মাঝে প্রত্যাশাই ভুল লেখে — যেমন “১৯০০ অধিবর্ষ।” তখন test ফেল করে, আর AI সঠিক কোডটিকেই “ঠিক” করতে গিয়ে নষ্ট করে ফেলে। তাই test-এর প্রতিটি প্রত্যাশিত মান মানুষের চোখে একবার যাচাই হওয়া চাই — ঠিক যেমন খাতা দেখার আগে উত্তরমালাটিই নির্ভুল হতে হয়।
১০ · মডিউল ৪-এর সমাপ্তি — বড় প্রোগ্রাম সাজানোর টুলবক্স
এই পর্বের সাথে মডিউল ৪ শেষ হলো। মডিউলের শুরুতে আপনার প্রোগ্রাম ছিল একটি ফাইলের, যা চালালে কিছু ছাপে আর শেষ হয়ে যায়। এখন আপনার হাতে আছে এমন প্রোগ্রাম বানানোর সব হাতিয়ার, যা ডেটা জমায়, কমান্ড-লাইন থেকে নির্দেশ নেয়, কয়েকটি মডিউলে ভাগ থাকে, ভুলের জন্য প্রস্তুত থাকে — আর নিজের সঠিকতা প্রমাণ করতে পারে।
| পর্ব | বিষয় | এখন আপনি যা পারেন |
|---|---|---|
| ২৪ | ফাইল I/O | প্রোগ্রাম বন্ধ হলেও ডেটা টিকিয়ে রাখা |
| ২৫ | কমান্ড-লাইন আর্গুমেন্ট ও variadic | প্রোগ্রামকে অন্য টুলের সাথে জোড়া যায় এমন CLI বানানো |
| ২৬ | স্ট্যান্ডার্ড লাইব্রেরি | তৈরি, পরীক্ষিত ফাংশন চিনে ঠিক জায়গায় ব্যবহার |
| ২৭ | প্রিপ্রসেসর ও ম্যাক্রো | কোড compile-এর আগেই বদলানো — যেমন আজকের CHECK |
| ২৮ | ফাংশন পয়েন্টার ও callback | আচরণ বাইরে থেকে পাঠানো — আজকের নকল ঘড়ি |
| ২৯ | মডুলার প্রোগ্রাম | header, Makefile আর library দিয়ে বড় প্রজেক্ট সাজানো |
| ৩০ | এরর হ্যান্ডলিং | ভুল ইনপুট আর ব্যর্থতায় ভদ্রভাবে সামলানো |
| ৩১ | ডিবাগিং | gdb, sanitizer, Valgrind দিয়ে বাগের উৎস খোঁজা |
| ৩২ | টেস্টিং | সঠিকতা প্রমাণ করা, আর বাগ ফিরে আসা ঠেকানো |
খেয়াল করুন, আজকের পর্বে আগের প্রায় সবকটি পর্ব একসাথে কাজ করেছে: macro (২৭) দিয়ে harness, function pointer (২৮) দিয়ে নকল নির্ভরতা, Makefile (২৯) দিয়ে make test, exit code (২৫) দিয়ে CI-কে খবর, sanitizer (৩১) দিয়ে লুকানো মেমরি-ভুল। মডিউল ৫-এ নিজ হাতে linked list, stack, tree, hash table বানাবেন, যেখানে একটি ভুল pointer পুরো কাঠামো ভেঙে দেয়। প্রতিটির সাথে test লিখুন — খালি, একটি, অনেক উপাদান।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Knight Capital ২০১২: মাত্র ৪৫ মিনিটে প্রায় ৪৪ কোটি ডলারের ক্ষতি। এটি কি শুধু “একটি বাগ”, নাকি test ও deployment প্রক্রিয়ার ব্যর্থতা?
Knight Capital ছিল যুক্তরাষ্ট্রের শেয়ারবাজারের বড় একটি ট্রেডিং প্রতিষ্ঠান। ২০১২ সালের ১ আগস্ট সকালে বাজার খোলার পর তাদের স্বয়ংক্রিয় ট্রেডিং সিস্টেম অস্বাভাবিক হারে কেনাবেচা শুরু করে। প্রায় ৪৫ মিনিটে লক্ষ লক্ষ অনাকাঙ্ক্ষিত অর্ডার চলে যায়, আর প্রতিষ্ঠানটির ক্ষতি দাঁড়ায় প্রায় ৪৪০ মিলিয়ন ডলার। কয়েক দিনের মধ্যেই তাদের বাইরের বিনিয়োগ নিয়ে টিকে থাকতে হয়, আর পরের বছর অন্য একটি প্রতিষ্ঠানের সাথে একীভূত হয়।
পরে মার্কিন নিয়ন্ত্রক সংস্থার তদন্তে যা উঠে আসে, তা শিক্ষণীয়। নতুন একটি ফিচারের কোড কয়েকটি সার্ভারে বসানো হয়েছিল, কিন্তু একটি সার্ভারে পুরোনো কোড রয়ে গিয়েছিল। নতুন ফিচারটি একটি পুরোনো flag-কে নতুন অর্থে ব্যবহার করছিল। সেই flag পেয়ে পুরোনো সার্ভারটি বহু বছর আগে বন্ধ করা একটি পুরোনো ফিচার জাগিয়ে তোলে — যে কোড তখনো মুছে ফেলা হয়নি। আর সমস্যা চলাকালীন দ্রুত থামানোর কোনো পরিষ্কার পদ্ধতিও ছিল না।
লক্ষ করুন, এখানে কোনো একটি ফাংশনের >= বনাম > ভুল ছিল না। সমস্যা ছিল কয়েক স্তরে:
- মৃত কোড রেখে দেওয়া: যে কোড আর ব্যবহার হয় না, কিন্তু চালু করা যায় — সেটি একটি টাইম বোমা।
- একটি flag-এর অর্থ বদলানো: একই নাম, দুই সময়ে দুই অর্থ — পুরোনো আর নতুন অংশের মাঝে চুক্তি ভেঙে গেল।
- Deployment যাচাই না করা: সব সার্ভারে একই সংস্করণ আছে কি না, স্বয়ংক্রিয়ভাবে মেলানো হয়নি।
- End-to-end ও integration test-এর অভাব: “নতুন আর পুরোনো সংস্করণ পাশাপাশি চললে কী হয়” — এমন পরিস্থিতি পরীক্ষা হয়নি।
মূল উপলব্ধি: Unit test একটি ফাংশনকে রক্ষা করে, কিন্তু সিস্টেম ভাঙে প্রায়ই জোড়ায়, কনফিগারেশনে আর deployment-এ। Test pyramid-এর প্রতিটি স্তর আর একটি স্বয়ংক্রিয় প্রক্রিয়া — দুটোই দরকার।
প্র ০২SQLite-এর test কোড library-র নিজের কোডের চেয়ে শত শত গুণ বড়, আর তারা ১০০% branch coverage দাবি করে। এত বাড়াবাড়ি কেন — আর তবুও কি বাগ থাকে?
SQLite C-তে লেখা একটি ছোট database library, যা সম্ভবত পৃথিবীর সবচেয়ে বেশি ব্যবহৃত সফটওয়্যারগুলোর একটি — প্রায় প্রতিটি স্মার্টফোন, ব্রাউজার আর অসংখ্য অ্যাপের ভেতরে এটি আছে। আপনার ফোনের মেসেজ বা কন্টাক্ট হয়তো এখনই একটি SQLite ফাইলে আছে।
SQLite দল প্রকাশ্যে তাদের test পদ্ধতি লিখে রেখেছে। তাদের দাবি অনুযায়ী test কোডের পরিমাণ library-র নিজের কোডের কয়েকশো গুণ, আর তাদের একটি test suite দিয়ে library-র প্রতিটি শাখা অন্তত একবার চলে — অর্থাৎ ১০০% branch coverage, এমনকি আরও কঠোর একটি মাপকাঠিতেও (MC/DC, যা বিমানের সফটওয়্যারের মানদণ্ডে ব্যবহৃত হয়)। তারা fuzzing করে, মেমরি ফুরিয়ে যাওয়া আর মাঝপথে বিদ্যুৎ চলে যাওয়ার মতো পরিস্থিতি কৃত্রিমভাবে তৈরি করে পরীক্ষা করে।
কেন এত? কারণ ব্যবহারের পরিমাণ। শতকোটি ডিভাইসে চলা কোডের একটি বিরল বাগ — যা লাখে একবার ঘটে — প্রতিদিন হাজার হাজার মানুষের ডেটা নষ্ট করতে পারে। আর ডেটাবেসের বাগ মানে ডেটা হারানো, যা ফেরত আনা যায় না। এমন জায়গায় test-এর খরচ বাগের খরচের তুলনায় নগণ্য।
তবুও কি বাগ থাকে? হ্যাঁ, থাকে। SQLite-এও মাঝে মাঝে বাগ ধরা পড়ে — অনেকগুলো fuzzer-এর হাতেই। এখানেই অংশ ৭-এর শিক্ষা: ১০০% coverage মানে প্রতিটি শাখা চলেছে, কিন্তু প্রতিটি সম্ভাব্য ইনপুট আর অবস্থার মিশ্রণ নয়। একই লাইন ভিন্ন ডেটায় ভিন্ন আচরণ করতে পারে।
আপনার প্রজেক্টে এই মান লাগবে না। কিন্তু প্রশ্নটি করুন: “কত মানুষ ব্যবহার করবে, আর ভুল হলে ক্ষতি কতটা ফেরানো যায়?” অনুশীলনী প্রোগ্রাম আর মোবাইল ব্যাংকিংয়ের লেনদেন-ইঞ্জিনের test এক মাত্রার হতে পারে না।
মূল উপলব্ধি: Test-এর পরিমাণ একটি প্রকৌশল সিদ্ধান্ত — ঝুঁকি আর ব্যবহারের মাত্রার সাথে মিলিয়ে ঠিক করতে হয়। আর সবচেয়ে কঠোর test-ও সঠিকতার নিশ্চয়তা নয়, কেবল আস্থা অনেক গুণ বাড়ায়।
প্র ০৩অভিজ্ঞ প্রোগ্রামাররা যে বাগ বছরের পর বছর দেখেননি, একটি “বোকা” fuzzer কয়েক ঘণ্টায় তা কীভাবে খুঁজে পায়?
মানুষ test লেখার সময় ভাবেন কোড কীভাবে ব্যবহার হওয়ার কথা। যিনি parser লিখেছেন, তিনি জানেন ইনপুট দেখতে কেমন হয় — আর অবচেতনভাবেই সেই ছাঁচের ইনপুট দিয়ে test করেন। কিন্তু আক্রমণকারী, বা নিছক একটি নষ্ট ফাইল, সেই ছাঁচ মানে না। Fuzzer-এর কোনো ধারণাই নেই কোড কেমন হওয়ার কথা — আর এটাই তার শক্তি। সে এমন সব ইনপুট দেয়, যা কোনো যুক্তিবাদী মানুষ কখনো দিতেন না।
দ্বিতীয় কারণ পরিমাণ। একজন মানুষ দিনে কয়েক ডজন test লিখতে পারেন; fuzzer প্রতি সেকেন্ডে হাজার হাজার ইনপুট চালায়, দিনরাত, সপ্তাহের পর সপ্তাহ। তৃতীয় কারণ coverage-নির্দেশনা: আধুনিক fuzzer অন্ধভাবে চলে না; যে ইনপুট নতুন পথ খোলে সেটি রেখে আরও গভীরে যায়, যেন একটি গোলকধাঁধায় প্রতিটি নতুন মোড় চিহ্নিত করে এগোনো।
২০১৪ সালে OpenSSL library-তে ধরা পড়া Heartbleed এই ধরনের বাগের বিখ্যাত উদাহরণ। ক্লায়েন্ট একটি ছোট বার্তা পাঠিয়ে বলত “আমার বার্তা এত বাইট লম্বা, ফেরত পাঠাও।” সার্ভার সেই দৈর্ঘ্য যাচাই না করেই বিশ্বাস করত — ফলে ছোট বার্তার বদলে মেমরি থেকে অনেক বেশি তথ্য ফেরত যেত, যাতে কখনো কখনো পাসওয়ার্ড বা গোপন key থাকত। কোডটি বছর দুয়েক ধরে খোলা অবস্থায় ছিল, বহু মানুষের চোখের সামনে। পরে দেখানো হয়েছে, আধুনিক fuzzer আর sanitizer মিলে এই ধরনের ভুল খুব দ্রুত খুঁজে পায় — libFuzzer-এর প্রশিক্ষণ উপকরণেই Heartbleed আবার খুঁজে বের করার অনুশীলন রাখা আছে।
এ ধরনের ঘটনার পর বড় প্রতিষ্ঠানগুলো fuzzing-কে গুরুত্ব দিতে শুরু করে; OSS-Fuzz আজ শত শত open-source প্রজেক্ট দিনরাত fuzz করে। তবে fuzzer সব পারে না — সে crash দেখে, ভুল উত্তর (যেমন ভুল গ্রেড) ধরতে তাকে একটি property বা তুলনার উৎস দিতে হয়।
মূল উপলব্ধি: মানুষ ভাবে উদ্দেশ্য দিয়ে, fuzzer ভাবে সম্ভাবনা দিয়ে। দুটো মিলিয়েই সবচেয়ে শক্ত সুরক্ষা — আর যে কোড বাইরের ইনপুট পড়ে, তাকে fuzz না করে প্রকাশ করা এখন প্রায় দায়িত্বহীনতা।
প্র ০৪Dijkstra বলেছিলেন, “Testing দেখাতে পারে বাগ আছে, কখনো দেখাতে পারে না বাগ নেই।” তাহলে test লিখে লাভ কী?
ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra ১৯৭০ সালের দিকে কথাটি লেখেন: “Program testing can be used to show the presence of bugs, but never to show their absence.” কথাটি গাণিতিকভাবে নির্ভুল। দুটি int নেয় এমন একটি ছোট ফাংশনের সম্ভাব্য ইনপুট-জোড়া প্রায় ১.৮ × ১০১৯টি — সব test করা অসম্ভব। ১০০০টি test পাস করা মানে শুধু এই যে ওই ১০০০টি ক্ষেত্রে ঠিক আছে।
Dijkstra নিজে এর সমাধান দেখতেন গাণিতিক প্রমাণে — কোড লেখার সময়ই প্রমাণ করা যে সেটি spec মেনে চলে। এই ধারা আজ formal verification নামে বেঁচে আছে, আর সত্যিকারের সাফল্যও আছে: ফ্রান্সে তৈরি CompCert নামের একটি C compiler-এর সঠিকতা গাণিতিকভাবে প্রমাণিত; বিমান, রেল ও চিকিৎসা-যন্ত্রের কিছু সফটওয়্যারও এভাবে যাচাই হয়। কিন্তু এটি ব্যয়বহুল, বিশেষ দক্ষতা লাগে, আর প্রমাণ ততটুকুই ভালো যতটুকু spec ভালো।
তাহলে test কেন? কারণ প্রকৌশল নিশ্চয়তার খেলা নয়, ঝুঁকি কমানোর খেলা। ভালো বাছাই করা test — boundary, equivalence class, অবৈধ ইনপুট — সম্ভাব্য বাগের সবচেয়ে বড় অংশ কম খরচে ধরে। আর test-এর আসল শক্তি একবার প্রমাণে নয়, বারবার যাচাইয়ে: প্রতিটি পরিবর্তনের পর পুরোনো সব প্রতিশ্রুতি এক সেকেন্ডে আবার মেলানো যায়, যা কোনো হাতে-করা প্রমাণ করতে পারে না।
Dijkstra-র উক্তিটি একটি বিনয়ও শেখায়: “সব test পাস” মানে “এখনো কোনো ভুল পাইনি।” এই মানসিকতা থাকলে আপনি coverage-এর ফাঁক খুঁজবেন, fuzzer চালাবেন, mutation দিয়ে test-কে পরীক্ষা করবেন — কারণ আপনি জানেন সবুজ চিহ্ন প্রমাণ নয়, প্রমাণের একটি অংশ।
মূল উপলব্ধি: Test আত্মবিশ্বাস দেয়, নিশ্চয়তা নয়। ভালো প্রকৌশলী দুটির পার্থক্য জানেন, আর ঝুঁকি অনুযায়ী test, review, sanitizer, fuzzing ও প্রয়োজনে formal পদ্ধতি একসাথে ব্যবহার করেন।
প্র ০৫কিছু test কখনো পাস করে, কখনো ফেল — কোড না বদলেও। এই “flaky test” কেন এত ক্ষতিকর, আর C-তে এর উৎস কী কী?
যে test একই কোডে কখনো পাস আর কখনো ফেল করে, তাকে বলে flaky test. শুনতে ছোট সমস্যা মনে হয়, কিন্তু এটি পুরো test ব্যবস্থার ভিত্তি — আস্থা — নষ্ট করে দেয়।
ঘটনাটি এভাবে ঘটে। CI লাল হলো। ডেভেলপার দেখলেন, “ও, এই test তো মাঝে মাঝেই ফেল করে” — আবার চালালেন, সবুজ হলো, merge করলেন। কয়েক সপ্তাহের মধ্যে দলের সবাই লাল দেখলে “আবার চালাও” বোতাম চাপা অভ্যাস করে ফেলেন। তারপর একদিন সত্যিকারের একটি বাগ লাল সংকেত দিল — আর কেউ গুরুত্ব দিল না। রাখালের “বাঘ এসেছে” গল্পের মতো। Google-এর মতো বড় প্রতিষ্ঠানও প্রকাশ্যে জানিয়েছে যে তাদের বিপুল test-এর একটি উল্লেখযোগ্য অংশে কখনো না কখনো এমন অনিশ্চিত আচরণ দেখা যায়, আর এটি সামলাতে আলাদা টুল ও নিয়ম বানাতে হয়েছে।
C প্রোগ্রামে flaky হওয়ার সাধারণ উৎস:
- Initialize না করা ভ্যারিয়েবল: মেমরিতে আগে যা ছিল তার ওপর ফল নির্ভর করে — একবার ০, একবার আবর্জনা। Sanitizer আর
-Wallএখানে সাহায্য করে। - Undefined behavior: সীমার বাইরে পড়া বা লেখা কখনো কিছুই করে না, কখনো crash — পাশে কী আছে তার ওপর নির্ভর করে।
- সময় ও random:
time()বা seed ছাড়াrand()-এর ওপর নির্ভর করা test. সমাধান অংশ ৫-এর নকল ঘড়ি আর নির্দিষ্ট seed. - Thread-এর race: দুটি thread কে আগে চলবে তার ওপর ফল নির্ভর করলে (পর্ব ৪৯)।
- Test-এর মধ্যে ভাগ করা অবস্থা: একটি test একটি global বা ফাইল বদলে রেখে গেল, পরেরটি সেটা ধরে নিল — ক্রম বদলালেই ফল বদলায়।
পেশাদার নিয়ম: flaky test-কে কখনো “এমনিই হয়” বলে মেনে নেবেন না। হয় সাথে সাথে ঠিক করুন, নয় আলাদা করে রাখুন এবং ঠিক করার কাজ নির্ধারণ করুন। প্রায়ই flaky test আসলে কোডের একটি সত্যিকারের লুকানো বাগের লক্ষণ।
মূল উপলব্ধি: একটি test-এর মূল্য তার নির্ভরযোগ্যতায়। যে সংকেতকে বিশ্বাস করা যায় না, তা সংকেতই নয় — তাই test নিজেই deterministic হতে হবে: একই কোড, একই ফল, প্রতিবার।
অনুশীলন
প্রথম তিনটিতে কোড লিখতে হবে না — test ডিজাইন করা আর ভাবাই আসল কাজ।
-
বাসভাড়ার test ডিজাইন: একটি ফাংশন
int fare_percent(int age)— ৫ বছরের কম হলে ০ (ফ্রি), ৫–১২ হলে ৫০, ১৩–৫৯ হলে ১০০, ৬০ বা বেশি হলে ৫০; বয়স ০-এর কম বা ১২০-এর বেশি হলে -১ (অবৈধ)। Equivalence class আর boundary value দিয়ে একটি পূর্ণ test তালিকা বানান।Class (৬টি): অবৈধ-নিচে (<০), ফ্রি (০–৪), অর্ধেক-শিশু (৫–১২), পূর্ণ (১৩–৫৯), অর্ধেক-বয়স্ক (৬০–১২০), অবৈধ-ওপরে (>১২০)।
Boundary জোড়া: (-১ → -১, ০ → ০), (৪ → ০, ৫ → ৫০), (১২ → ৫০, ১৩ → ১০০), (৫৯ → ১০০, ৬০ → ৫০), (১২০ → ৫০, ১২১ → -১)। মোট ১০টি test, যা প্রতিটি class-কেও ছুঁয়ে যায়।
লক্ষ করুন, “অর্ধেক” ফল দুটি আলাদা class থেকে আসে — তাই দুটিকেই আলাদা test করতে হবে।
-
Mutant শিকার: প্রথম চালানো যায় এমন উদাহরণে (
grade_test.c) একে একে তিনটি বদল করুন, প্রতিবার আগেরটি ফিরিয়ে: (ক)m > 100→m >= 100(খ)m >= 33→m >= 32(গ)m < 0→m <= 0. প্রতিবার কোন test ফেল করল?(ক)
grade(100) == 'A'ফেল — ১০০ এখন অবৈধ হয়ে গেছে।(খ)
grade(32) == 'F'ফেল — ৩২ এখন পাস দেখাচ্ছে।(গ)
grade(0) == 'F'ফেল — ০ এখন অবৈধ।তিনটি mutant-ই “মারা গেল”, প্রতিবার ঠিক একটি boundary test দিয়ে। এবার পরীক্ষা হিসেবে শুধু
grade(50)আরgrade(90)রেখে বাকি test মুছে দেখুন — তিনটি mutant-ই বেঁচে যাবে। এটাই mutation testing-এর শিক্ষা। -
Testable নকশা: একটি ফাংশন
void show_average(void)একটি ফাইল খুলে নম্বরগুলো পড়ে, গড় বের করে, আর স্ক্রিনে ছাপে। Test করা যায় এমনভাবে এটিকে কয়েকটি ফাংশনে ভাঙুন — শুধু ফাংশনের নাম আর parameter লিখুন।একটি ভালো নকশা:
int read_marks(FILE *in, int *arr, int cap)— যেকোনোFILE *থেকে পড়ে, কয়টি পড়ল তা ফেরত দেয়। Test-এtmpfile()-এ লেখা দিয়ে পাঠানো যায়।int average(const int *arr, int n, double *out)— pure function;n == 0হলে error ফেরত দেয়। এটিই সবচেয়ে বেশি test পাবে: শূন্য, এক, অনেক, বড় মান।void print_average(FILE *out, double avg)— শুধু ছাপা; test-এ অস্থায়ী ফাইলে ছেপে লেখা মেলানো যায়।show_average()এখন শুধু এই তিনটিকে জোড়া দেয় — এত পাতলা যে এর জন্য একটি end-to-end test-ই যথেষ্ট।
-
Leap year শেষ করুন: দ্বিতীয় চালানো যায় এমন উদাহরণে (
leap_test.c) বাগটি ঠিক করুন, টেবিলে{1600, 1}আর{1, 0}যোগ করুন। তারপর একটি property test যোগ করুন: ১ থেকে ২০০০ পর্যন্ত প্রতিটি বছরের জন্যis_leap(y) == is_leap(y + 400).ঠিক করা ফাংশন:
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;টেবিল এখন ৭টি সারি — সব পাস। Property অংশটিmain-এর শেষে এভাবে যোগ করা যায়:for (int y = 1; y <= 2000; y++) if (is_leap(y) != is_leap(y + 400)) { printf("FAIL property at %d\n", y); failed++; }এক লাইনের নিয়ম, অথচ ২০০০টি পরীক্ষা। এবার ইচ্ছা করে বাগটি ফিরিয়ে আনুন (৪০০-র নিয়ম মুছে দিন) — মজার ব্যাপার, property test পাস করবে! কারণ ভুল ফাংশনটিও ৪০০ বছর পরপর একই আচরণ করে। শুধু টেবিলের ২০০০ আর ১৬০০ ফেল করবে। শিক্ষা: একটি property সব ভুল ধরে না; উদাহরণ আর property — দুটোই দরকার।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি বাগ রিপোর্ট পাওয়ার পর পেশাদার পদক্ষেপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🐞 বাগটি নিজের কম্পিউটারে আবার ঘটিয়ে দেখা (reproduce)
- 🔴 এমন একটি test লেখা যা বাগটির কারণে ফেল করে
- 🔧 কোড ঠিক করা
- 🟢 নতুন test সহ পুরো suite চালিয়ে সব সবুজ দেখা
- 📦 সংশোধন আর test একসাথে commit — CI এখন থেকে প্রতিবার পাহারা দেবে
Spec: “৩৩ বা তার বেশি পেলে পাস।” পাস/ফেল সীমানা পরীক্ষার জন্য সবচেয়ে কার্যকর জোড়া কোনটি?
- ১০ আর ৯০
- ৩৩ আর ৩৪
- ৩২ আর ৩৩
- ০ আর ১০০
Test প্রোগ্রামের শেষে return failed != 0; লেখার মূল উদ্দেশ্য কী?
- ফেলের সংখ্যা স্ক্রিনে দেখানো
- make test বা CI-কে জানানো যে কোনো test ফেল করেছে
- প্রোগ্রাম দ্রুত শেষ করা
- মেমরি পরিষ্কার করা
CHECK কেন ফাংশন না হয়ে macro?
- যাতে ফেল হলে CHECK লেখার জায়গার ফাইল, লাইন আর শর্তের লেখাটিই ছাপা যায়
- Macro ফাংশনের চেয়ে অনেক দ্রুত চলে
- ফাংশন কোনো গণনা বাড়াতে পারে না
- C-তে ফাংশনের ভেতরে if লেখা যায় না
TDD-এ “Red” ধাপে নতুন test-টি প্রথমবারেই পাস করে গেল। এর সবচেয়ে সম্ভাব্য অর্থ কী?
- কোড নিখুঁত, পরের ধাপে যাওয়া যায়
- Compiler-এ সমস্যা আছে
- সাথে সাথে refactor করা উচিত
- Test-টি হয়তো কিছুই যাচাই করছে না, অথবা আচরণটি আগেই আছে — খতিয়ে দেখা দরকার
একটি মডিউলের code coverage ১০০%. কোন কথাটি সত্য?
- মডিউলে আর কোনো বাগ নেই
- Test চলার সময় প্রতিটি লাইন অন্তত একবার চলেছে — এর বেশি কিছু নিশ্চিত নয়
- প্রতিটি সম্ভাব্য ইনপুট পরীক্ষা হয়েছে
- এখন আর fuzzing-এর দরকার নেই
AI is_leap()-এর জন্য এই test লিখেছে: CHECK(is_leap(1900) == (1900 % 4 == 0)); সমস্যা কোথায়?
- এটি compile হবে না
- একটি test-এ বেশি শর্ত
- প্রত্যাশিত ফল spec থেকে নয়, সূত্র নকল করে বানানো — আর সেই সূত্রও ভুল
- ১৯০০ খুব পুরোনো বছর, test-এ রাখা অর্থহীন
একটি ফাংশন নিজেই scanf দিয়ে নম্বর পড়ে, হিসাব করে, printf দিয়ে ছাপে। Test করা সহজ করতে সবচেয়ে ভালো পদক্ষেপ কোনটি?
- হিসাবের অংশটি একটি আলাদা pure function-এ সরিয়ে নেওয়া, I/O বাইরে রাখা
- প্রতিবার হাতে কয়েকটি নম্বর দিয়ে চালিয়ে দেখা
- ফাংশনের ভেতরে কয়েকটি assert বসানো
- ইনপুটটি একটি global ভ্যারিয়েবলে রাখা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Test হলো কোডে লেখা প্রতিশ্রুতি — প্রত্যাশিত ফল আসে spec থেকে, কোড দেখে নয়।
- Test case বাছুন equivalence class আর boundary দিয়ে; মনে রাখুন শূন্য, এক, অনেক, সর্বোচ্চ, অবৈধ।
CHECKmacro + পাস/ফেল গণনা + ফেলে শূন্য নয় এমন exit code = একটি পূর্ণ test harness;make testআর CI সেটিকে প্রতিবার চালায়।- হিসাব ভেতরে, I/O বাইরে; ঘড়ি, ফাইল, random বাইরে থেকে দিন। TDD: red → green → refactor, আর প্রতিটি বাগের জন্য একটি regression test.
- Fuzzing অদেখা ইনপুট খোঁজে, coverage অদেখা কোড দেখায় — কিন্তু ১০০% coverage মানে সঠিক নয়। AI-এর test-এ tautology আর নকল সূত্র খুঁজুন; mutant দিয়ে test-কে test করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৩ · লিংকড লিস্টপরবর্তী পর্বমডিউল ৫-এর শুরু — pointer দিয়ে জোড়া নোডের তালিকা।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Software Testing & QAগভীরে যানEquivalence partitioning, boundary value, decision table, state transition — পেশাদার QA-র পুরো পদ্ধতি।
- Software Engineering & Gitপাশের ট্র্যাকCode review, branch, pull request — দলে test কীভাবে কাজের অংশ হয়।
- Cloud & DevOpsপাশের ট্র্যাকCI/CD pipeline, স্বয়ংক্রিয় deployment আর নিরাপদে নতুন সংস্করণ চালু করা।