সরাসরি পর্বে যান
পর্ব ৪৮ · ৫৫-এর মধ্যে · মডিউল ৭

টেস্টিং ও ডিবাগিং — প্রমাণ দিয়ে কোডে ভরসা, পদ্ধতি দিয়ে বাগ খোঁজা

Testing and Debugging — Trust Code by Evidence, Hunt Bugs by Method

Taught in Bangla, this lesson covers testing and debugging: the testing pyramid and Arrange-Act-Assert, a home-made CHECK macro, GoogleTest, Catch2 and doctest, testable design and edge cases, TDD, CTest, CI and coverage, then debugging scientifically with GDB, core dumps and git bisect.

এই পর্বে যা শিখবেন

  • কেন টেস্ট লেখা হয়, testing pyramid, আর একটি unit test-এর তিন অংশ
  • Framework ছাড়া নিজের ছোট test harness, তারপর GoogleTest, Catch2 আর doctest
  • টেস্ট করা যায় এমন নকশা, edge case-এর চেকলিস্ট, TDD, property-based testing, CTest, CI আর coverage
  • বৈজ্ঞানিক পদ্ধতিতে ডিবাগিং: compiler error পড়া, ঠিকভাবে print করা, GDB, core dump আর git bisect
  • AI-এর লেখা টেস্টের দুর্বলতা চেনা, আর AI দিয়ে edge case ও stack trace বোঝা

১ · টেস্ট মানে চালানো যায় এমন প্রতিশ্রুতি

এই কোর্সে আপনি অনেক প্রোগ্রাম লিখেছেন। প্রতিবার লেখার পর কী করেছেন? চালিয়ে দেখেছেন, স্ক্রিনে ফল দেখে মনে মনে বলেছেন “হ্যাঁ, ঠিক আছে।” এই কাজটিই আসলে একটি টেস্ট — শুধু সমস্যা হলো, সেটি আপনার মাথায় ছিল, কোডে নয়। পরের সপ্তাহে কোড বদলালে আবার হাতে চালাতে হবে, আবার মনে করতে হবে কোন ফল ঠিক। দশটি ফাংশন হলে কাজটি বিরক্তিকর, এক হাজারটি হলে অসম্ভব।

স্বয়ংক্রিয় টেস্ট সেই “মনে মনে যাচাই”-কে কোডে লিখে ফেলে: “discounted(100000) ডাকলে ফল হবে 90000.” এটি একটি চালানো যায় এমন প্রতিশ্রুতি। যতবার খুশি, যেকোনো মেশিনে, এক সেকেন্ডের কম সময়ে মেশিন নিজেই যাচাই করে দেয় প্রতিশ্রুতিটি এখনো রাখা হচ্ছে কি না। সবচেয়ে ছোট এই ধরনের যাচাইয়ের নাম unit test — একটি ফাংশন বা class-কে আলাদা করে পরীক্ষা।

একটি গার্মেন্টস কারখানার কোয়ালিটি কন্ট্রোল টেবিলের কথা ভাবুন। প্রতিটি ব্যাচের শার্ট এক এক করে মাপা হয় — কলার, হাতা, বুকের মাপ একটি লিখিত তালিকার সাথে মিলিয়ে। মেশিন বদলালে, নতুন কর্মী এলে, নতুন কাপড় এলেও তালিকা একই থাকে। তালিকাটি না থাকলে প্রতিবার কেউ “চোখে দেখে” বলবে ঠিক আছে, আর ভুল ধরা পড়বে বিদেশি ক্রেতার গুদামে। টেস্ট হলো আপনার কোডের সেই লিখিত মাপের তালিকা।

টেস্ট লেখার তিনটি বড় কারণ আছে, আর তিনটিই সময়ের সাথে বাড়ে:

Regression ধরা

আজ ঠিক করা বাগ ছয় মাস পরে অন্য কারও পরিবর্তনে ফিরে আসতে পারে। একবার যে বাগ ধরা পড়েছে, তার জন্য লেখা regression test সেটি আর কখনো নিঃশব্দে ফিরতে দেয় না।

পুরোনো ভুল আর ফিরবে না

নির্ভয়ে বদলানো

পর্ব ২৭-এ ডিজাইন নীতি দেখেছেন; কোড সুন্দর করতে তাকে বারবার নতুন করে সাজাতে হয়, যার নাম refactoring. টেস্ট না থাকলে প্রতিটি পরিবর্তন জুয়া। টেস্ট থাকলে সবুজ দেখা মানে আচরণ বদলায়নি।

ভয় কমে, গতি বাড়ে

জীবন্ত ডকুমেন্টেশন

“খালি কার্টে মোট টাকা শূন্য”, “৮০ নম্বরে A+” — টেস্টের নামগুলো পড়লেই কোডের নিয়ম বোঝা যায়। সাধারণ মন্তব্য পুরোনো হয়ে মিথ্যা হতে পারে, কিন্তু টেস্ট মিথ্যা হলে লাল হয়ে যায়।

যে ডকুমেন্ট নিজেই যাচাই হয়

এই পর্বের দ্বিতীয় অর্ধেক অন্য দিক থেকে আসে। টেস্ট যখন লাল হয়, বা ব্যবহারকারী জানান “রিপোর্ট crash করছে”, তখন কারণ খুঁজতে হয়। এর নাম ডিবাগিং, আর এটি অনুমানের খেলা নয়, একটি পদ্ধতি — ঠিক বিজ্ঞানের পরীক্ষাগারের মতো: পর্যবেক্ষণ, অনুমান, পরীক্ষা, পরিসর ছোট করা। টেস্টিং আর ডিবাগিং একই মুদ্রার দুই পিঠ: টেস্ট বাগকে সামনে আনে, ডিবাগিং তার শিকড় খোঁজে, আর শেষে একটি নতুন টেস্ট সেই শিকড়কে চিরকালের জন্য পাহারা দেয়।

C জানলে

মূল ধারণা একই — assert, নিজের CHECK macro, GDB, sanitizer, core dump সবই C-তেও আছে। C++-এ পার্থক্য হলো framework-গুলো class, template আর exception কাজে লাগায় (যেমন “এই কোড exception ছোড়ে কি না” যাচাই), fixture-এর পরিষ্কারের কাজ RAII দিয়ে হয়, আর __FILE__/__LINE__ macro-র জায়গায় std::source_location আছে। গভীরে দেখতে C কোর্সের টেস্টিং আর ডিবাগিং পর্ব দুটি পড়ুন।

২ · টেস্টিং পিরামিড আর একটি টেস্টের তিন অংশ

সব টেস্ট এক রকম নয়। একটি দোকানের সফটওয়্যারের কথা ভাবুন — ছাড়ের হিসাব, কার্ট, ডেটাবেস, আর ব্রাউজারে পুরো কেনাকাটা। কোনটি কীভাবে পরীক্ষা করবেন, তার একটি পরিচিত নকশা আছে, যার নাম testing pyramid:

স্তরকী পরীক্ষা করেগতিকতগুলো
Unit test (নিচে, চওড়া)একটি ফাংশন বা class, বাইরের কিছু ছাড়া — যেমন discounted()মিলিসেকেন্ডের ভগ্নাংশশত বা হাজার
Integration test (মাঝে)কয়েকটি অংশ একসাথে — যেমন কার্ট আর আসল ডেটাবেস ফাইলমিলিসেকেন্ড থেকে সেকেন্ডডজনখানেক
End-to-end test (ওপরে, সরু)পুরো প্রোগ্রাম ব্যবহারকারীর মতো — ইনপুট ফাইল থেকে রিপোর্ট পর্যন্তসেকেন্ড বা মিনিটহাতে গোনা কয়েকটি

পিরামিডের আকৃতির কারণ সরল: নিচের টেস্ট দ্রুত, স্থির, আর ব্যর্থ হলে ঠিক কোন ফাংশনে সমস্যা তা সাথে সাথে বলে দেয়। ওপরের টেস্ট বাস্তবের কাছাকাছি, কিন্তু ধীর, ভঙ্গুর, আর ব্যর্থ হলে শুধু বলে “কোথাও কিছু ভুল।” তাই বেশিরভাগ যাচাই নিচে রাখুন, আর ওপরে শুধু এটুকু নিশ্চিত করুন যে অংশগুলো একসাথে জোড়া লাগে। উল্টো আকৃতি — শুধু ধীর end-to-end টেস্ট — দলগুলো মজা করে “আইসক্রিম কোন” বলে, আর সেখানে টেস্ট চালাতে এত সময় লাগে যে কেউ চালায় না।

একটি unit test-এর শরীর — Arrange, Act, Assert

প্রায় প্রতিটি ভালো unit test তিনটি অংশে ভাগ করা যায়, যার নাম Arrange–Act–Assert: আগে অবস্থা সাজানো, তারপর ঠিক একটি কাজ করা, শেষে ফল যাচাই। প্রতিটি টেস্টের একটি স্পষ্ট নাম থাকে, যা একটি আচরণ বর্ণনা করে — এক কথায় একটি test case. নিয়মটি হলো: দোকানে ১০০০ টাকা বা বেশি কেনাকাটায় ১০% ছাড়। টাকা আমরা পয়সায় রাখছি (১ টাকা = ১০০ পয়সা), যাতে double-এর দশমিক ভুল না আসে। ধাপে ধাপে দেখুন:

1void discount_on_exact_boundary() {2    // Arrange: exactly 1000 Tk3    long long price = 100'000;  // poisha4    // Act: one call, nothing else5    long long got = discounted(price);6    // Assert: compare with the promise7    CHECK(got == 90'000);8}
টেস্টের অবস্থা
price
got
ফল
  1. নাম — কোন আচরণ? টেস্টের নাম একটি বাক্যের মতো: “ঠিক সীমানায় ছাড়।” ব্যর্থ হলে শুধু নামটি পড়েই বোঝা যায় কোন প্রতিশ্রুতি ভেঙেছে। test1, test2 নাম কিছুই বলে না।
  2. Arrange — অবস্থা সাজানো। ইনপুট, দরকারি object, fake — সব এখানে তৈরি হয়। আমরা ইচ্ছা করে ঠিক ১০০০ টাকা নিয়েছি, কারণ সীমানাতেই বাগ সবচেয়ে বেশি লুকায়।
  3. Act — ঠিক একটি কাজ। যে আচরণ পরীক্ষা করছি, শুধু সেটি চালানো। একটি টেস্টে পাঁচটি ভিন্ন কাজ করলে ব্যর্থ হলে বোঝা যায় না কোনটি দায়ী।
  4. Assert — প্রতিশ্রুতির সাথে মেলানো। প্রত্যাশিত মান 90'000 আমরা হাতে হিসাব করে লিখেছি, কোড থেকে কপি করিনি। ফল মিলল, তাই PASS.
  5. টেস্ট কেন মূল্যবান — যখন লাল হয়। ধরুন কেউ শর্তে >=-এর জায়গায় > লিখে ফেলল। ১০০০ টাকায় আর ছাড় হয় না, got হয় 100000, আর টেস্ট লাইন নম্বরসহ FAIL দেখায়। যে টেস্ট কখনো লাল হতে পারে না, সেটি কিছুই পাহারা দেয় না।

৩ · Framework ছাড়া প্রথম টেস্ট — assert আর ছোট CHECK macro

টেস্ট লিখতে কোনো বড় টুল লাগে না। সবচেয়ে সরল যন্ত্রটি standard library-তেই আছে: <cassert>-এর assert. assert(discounted(0) == 0); লিখলে শর্ত মিথ্যা হলে প্রোগ্রাম ফাইলের নাম, লাইন আর শর্তটি ছাপিয়ে থেমে যায়। কিন্তু টেস্টের কাজে এর দুটি দুর্বলতা আছে। এক, প্রথম ব্যর্থতাতেই সব থামে — বাকি নয়টি টেস্টের খবর পাওয়া যায় না। দুই, NDEBUG macro সংজ্ঞায়িত থাকলে (অনেক release build-এ থাকে) সব assert নিঃশব্দে উধাও হয়, তাই টেস্ট আসলে কিছুই যাচাই করে না।

সমাধান: নিজের একটি ছোট test harness — যে কোড টেস্ট চালায়, পাস-ফেল গোনে আর ব্যর্থতার জায়গা জানায়। নিচে মাত্র কয়েক লাইনে একটি। পর্ব ৪২-এর std::source_location দিয়ে লাইন নম্বর আসে, আর একটি ছোট macro শর্তটিকে লেখা হিসেবেও ধরে রাখে (#expr মানে “শর্তটির লেখা রূপ”)। পরীক্ষার বিষয় দোকানের ছাড়, আর প্রতিটি টেস্ট একটি করে edge case:

#include <print>
#include <source_location>

int passed = 0, failed = 0;
void check(bool ok, const char* what,
           std::source_location loc = std::source_location::current()) {
    if (ok) { ++passed; return; }
    ++failed; std::println("FAIL line {}: {}", loc.line(), what);
}
#define CHECK(expr) check((expr), #expr)

long long discounted(long long p) {   // poisha; 10% off from 1000 Tk
    if (p < 0) return -1;               // invalid input
    return p >= 100'000 ? p - p / 10 : p;
}
int main() {
    CHECK(discounted(0) == 0);                    // empty cart
    CHECK(discounted(99'999) == 99'999);          // just below
    CHECK(discounted(100'000) == 90'000);         // on the boundary
    CHECK(discounted(-500) == -1);                // invalid
    CHECK(discounted(5'000'000'000) == 4'500'000'000);  // very large
    std::println("{} passed, {} failed", passed, failed);
    return failed == 0 ? 0 : 1;
}

আউটপুট একটি লাইন: 5 passed, 0 failed. পাঁচটি টেস্টই পাস করেছে, তাই কোনো FAIL লাইন ছাপা হয়নি। খেয়াল করুন তিনটি জিনিস। প্রথমত, check-এর শেষ প্যারামিটারের default মান source_location::current() — default argument ডাকার জায়গায় হিসাব হয়, তাই লাইন নম্বর আসে main-এর ভেতরের সেই CHECK লাইন থেকে, check-এর ভেতর থেকে নয়। দ্বিতীয়ত, assert-এর মতো প্রথম ব্যর্থতায় থামে না; সব টেস্ট চলে, শেষে হিসাব। তৃতীয়ত, main ব্যর্থতা থাকলে 1 ফেরত দেয় — এই exit code দেখেই CTest আর CI বোঝে টেস্ট পাস করেছে কি না (অংশ ৬)। নিজে পরীক্ষা করুন: >= বদলে > লিখে চালান — তৃতীয় টেস্টের লাইন নম্বরসহ একটি FAIL দেখবেন, আর গণনা হবে 4 passed, 1 failed.

এখানে টেস্টগুলো কোন ইনপুট বেছেছে, সেটিই আসল শিক্ষা। 99'999 আর 100'000 — সীমানার দুই পাশ। 0 — খালি কার্ট। -500 — অবৈধ ইনপুট। পাঁচ কোটি টাকা (৫০০ কোটি পয়সা) — int-এর সীমা ছাড়ানো বিশাল মান, যেখানে টাইপ ছোট হলে overflow হতো (পর্ব ৪৩)। এলোমেলো দশটি সংখ্যা পরীক্ষা করার চেয়ে এই পাঁচটি অনেক বেশি কাজের। অংশ ৫-এ এর পুরো চেকলিস্ট দেখবেন।

৪ · Test framework — GoogleTest, Catch2 আর doctest

নিজের harness দিয়ে শেখা ভালো, কিন্তু বাস্তব প্রজেক্টে একটি test framework ব্যবহার হয়। এটি আপনার CHECK-এর মতোই, সাথে অনেক সুবিধা: প্রতিটি টেস্ট আলাদা নামে নিবন্ধন, ব্যর্থ হলে দুই পাশের মান ছাপানো, নাম দিয়ে বাছাই করে চালানো, exception যাচাই, আর CI-র জন্য XML রিপোর্ট। C++-এ তিনটি framework সবচেয়ে পরিচিত। নিচের কোডগুলো চালানোর জন্য নয় — এগুলোর জন্য library ইনস্টল করে link করতে হয় (পর্ব ৪৭-এর CMake দিয়ে)।

GoogleTest (gtest) Google-এর তৈরি, বড় প্রজেক্টে সবচেয়ে বেশি ব্যবহৃত। প্রতিটি টেস্ট TEST(দল, নাম). যাচাইয়ের দুই পরিবার: EXPECT_* ব্যর্থ হলে রিপোর্ট করে কিন্তু টেস্ট চালিয়ে যায়, ASSERT_* ব্যর্থ হলে সেই টেস্ট ফাংশন সেখানেই থামায়।

#include <gtest/gtest.h>

TEST(Discount, AppliesFromThousandTaka) {
    EXPECT_EQ(discounted(100'000), 90'000);
    EXPECT_EQ(discounted(99'999), 99'999);
}

class CartTest : public ::testing::Test {
protected:
    Cart cart;                         // fresh for every TEST_F
    void SetUp() override { cart.add({"Rice", 8'000, 5}); }
};

TEST_F(CartTest, TotalOfOneLine) {
    ASSERT_EQ(cart.size(), 1u);        // stop if setup failed
    EXPECT_EQ(cart.total(), 40'000);
}

CartTest একটি test fixture: কয়েকটি টেস্টের একই প্রস্তুতি এক জায়গায়। প্রতিটি TEST_F-এর জন্য framework একটি নতুন CartTest object বানায়, তাই এক টেস্টের পরিবর্তন অন্যটিতে লাগে না। পরিষ্কারের কাজ destructor বা TearDown()-এ — অর্থাৎ RAII (পর্ব ১৭)।

Catch2 সহজ লেখার জন্য জনপ্রিয়। টেস্টের নাম সাধারণ বাক্য। REQUIRE ব্যর্থ হলে থামে (gtest-এর ASSERT-এর মতো), CHECK চালিয়ে যায় (EXPECT-এর মতো)। আর তুলনাটি সাধারণ C++ == দিয়েই লেখা হয় — Catch2 নিজে দুই পাশ আলাদা করে ছাপায়।

#include <catch2/catch_test_macros.hpp>   // Catch2 v3

TEST_CASE("discount starts at 1000 Tk", "[shop]") {
    REQUIRE(discounted(100'000) == 90'000);
    CHECK(discounted(99'999) == 99'999);
}

TEST_CASE("cart total", "[shop]") {
    Cart cart;                     // re-run for each SECTION
    SECTION("empty cart is zero") {
        REQUIRE(cart.total() == 0);
    }
    SECTION("one line") {
        cart.add({"Rice", 8'000, 5});
        REQUIRE(cart.total() == 40'000);
    }
}

SECTION Catch2-এর নিজস্ব কৌশল: প্রতিটি SECTION-এর জন্য পুরো TEST_CASE শুরু থেকে আবার চলে, তাই Cart cart; প্রতিবার নতুন। আলাদা fixture class না লিখেই একই প্রস্তুতি ভাগ করা যায়।

doctest Catch2-এর মতো লেখা, কিন্তু খুব হালকা আর দ্রুত compile হয়। এর বিশেষত্ব: টেস্ট মূল কোডের পাশেই লেখা যায়, আর একটি flag দিয়ে release build থেকে সব টেস্ট বাদ দেওয়া যায়।

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

TEST_CASE("discount boundary") {
    CHECK(discounted(100'000) == 90'000);
    CHECK_THROWS_AS(parse_money("12.5.0"), std::invalid_argument);
}

CHECK_THROWS_AS যাচাই করে যে ভুল ইনপুটে ঠিক নির্দিষ্ট exception ছোড়া হয়েছে (পর্ব ৩৭)। GoogleTest-এ এর সমতুল্য EXPECT_THROW, Catch2-এ REQUIRE_THROWS_AS.

EXPECT নাকি ASSERT?

নিয়মটি সহজ: পরের লাইনগুলো যদি এই শর্তের ওপর নির্ভর করে, তবে থামানো যাচাই (fatal assertion: ASSERT_*, REQUIRE); নইলে non-fatal assertion (EXPECT_*, CHECK). যেমন vector-এর মাপ ৩ কি না আগে ASSERT করুন, কারণ মাপ ভুল হলে পরের v[2] নিজেই UB. আর তিনটি স্বাধীন মান পরীক্ষায় EXPECT, যাতে একবারে সব ভুল দেখা যায়।

৫ · টেস্ট করা যায় এমন নকশা — আর edge case-এর চেকলিস্ট

অনেকে বলেন “আমার কোড টেস্ট করা কঠিন।” প্রায় সবসময় এর মানে, কোডের নকশায় সমস্যা আছে। যে ফাংশন নিজেই ফাইল খোলে, ঘড়ি দেখে, random সংখ্যা বানায়, global ভ্যারিয়েবল বদলায় আর স্ক্রিনে লেখে — তাকে আলাদা করে পরীক্ষা করার কোনো উপায় নেই। Testability তাই নকশার একটি গুণ, আর তার চারটি সরল নিয়ম আছে।

  1. হিসাবকে pure function বানান। একই ইনপুটে সবসময় একই ফল, কোনো side effect নেই। discounted(long long) এমনই — টেস্ট করতে কিছু সাজাতে হয় না। ফাইল পড়া, হিসাব করা আর ছাপানো তিনটি আলাদা ফাংশনে রাখুন; মাঝেরটিই সবচেয়ে বেশি টেস্ট পায়।
  2. বাইরের জিনিস interface দিয়ে দিন। পর্ব ২৬-এ দেখেছেন, OrderService শুধু একটি Notifier& চেনে, আর টেস্টে আমরা SMS-এর বদলে একটি FakeNotifier দিয়েছিলাম। এটাই dependency injection, আর fake-টি একটি test double. ডেটাবেস, নেটওয়ার্ক, পেমেন্ট গেটওয়ে — সবকিছুর জন্য একই কৌশল। যে test double মনে রাখে কে কোন argument দিয়ে কতবার ডাকল, তাকে বলে mock; GoogleTest-এর সঙ্গী gMock এগুলো বানাতে সাহায্য করে।
  3. সময় আর random-কে বাইরে থেকে দিন। “রাত ১২টার পরে অর্ডারে ডেলিভারি চার্জ দ্বিগুণ” — এই নিয়ম ফাংশনের ভেতরে system_clock::now() ডাকলে টেস্ট করতে আপনাকে মাঝরাত পর্যন্ত জেগে থাকতে হবে। পর্ব ৪১-এর ঘড়ি আর random engine প্যারামিটার হিসেবে নিন। টেস্টে একটি স্থির সময়, আর একটি নির্দিষ্ট seed.
  4. লুকোনো global অবস্থা নয়। global variable-এ রাখা অবস্থা এক টেস্ট থেকে আরেক টেস্টে চুইয়ে পড়ে, আর টেস্টের ক্রম বদলালে ফল বদলায়। উপরের harness-এর passed/failed ব্যতিক্রম, কারণ সেগুলো শুধু গণনা — যাচাই করা কোডের অংশ নয়।
// কঠিন: ভেতরে লুকোনো ঘড়ি
long long delivery_fee(long long base) {
    auto now = std::chrono::system_clock::now();   // টেস্ট নিয়ন্ত্রণ করতে পারে না
    ...
}
// সহজ: সময় বাইরে থেকে আসে
long long delivery_fee(long long base, std::chrono::hours hour_of_day);
// টেস্ট: CHECK(delivery_fee(6'000, 23h) == 6'000);
//        CHECK(delivery_fee(6'000, 0h)  == 12'000);

Edge case-এর ছয় ঘর — parse_money দিয়ে

ধরুন দোকানের একটি ফাংশন ব্যবহারকারীর লেখা দাম পড়ে পয়সায় রূপান্তর করে: parse_money("12.50") ফেরত দেয় 1250. কোন কোন ইনপুটে টেস্ট লিখবেন? বাগ সাধারণত সাধারণ ইনপুটে নয়, কিনারায় থাকে — এদের নাম edge case. প্রায় যেকোনো ফাংশনের জন্য ছয়টি ঘর মনে রাখুন:

ঘরparse_money-তে উদাহরণযে বাগ ধরে
খালি"", শুধু ফাঁকা জায়গাখালি string-এ s[0] পড়া
এক"5", ".5"“অন্তত দুটি অঙ্ক থাকবে” ধরে নেওয়া
অনেক"1,250.75" (কমা সহ)বড় সংখ্যার বিন্যাস না বোঝা
সীমানা"0.99", "0.01", "12.5" বনাম "12.50"দশমিকের পরে একটি অঙ্ক হলে ১০ দিয়ে গুণ ভুলে যাওয়া
অবৈধ"12.5.0", "abc", "-3", "1.234"ভুল ইনপুট চুপচাপ মেনে নেওয়া; পয়সার নিচে ভগ্নাংশ
বিশাল২০ অঙ্কের একটি সংখ্যাoverflow (পর্ব ৪৩); std::from_chars এখানে result_out_of_range জানায়

অবৈধ ইনপুটে কী হবে, সেটি টেস্ট লেখার আগেই ঠিক করতে হয় — exception (পর্ব ৩৭), নাকি std::expected, নাকি std::optional (পর্ব ৩৮)। টেস্ট লিখতে গিয়ে প্রায়ই দেখা যায় এই সিদ্ধান্তটি কেউ নেয়নি। এটিও টেস্টের একটি উপকার: অস্পষ্ট নিয়ম সামনে চলে আসে।

৬ · TDD, property, fuzzing — আর CTest, CI ও coverage

Test-driven development — আগে টেস্ট

Test-driven development (TDD) ক্রম উল্টে দেয়: কোড লেখার আগে টেস্ট। চক্রটির নাম red–green–refactor. লাল: এখনো নেই এমন আচরণের জন্য একটি ছোট টেস্ট লিখুন, চালিয়ে ব্যর্থ হতে দেখুন — এতে প্রমাণ হয় টেস্টটি সত্যিই কিছু যাচাই করে। সবুজ: টেস্ট পাস করানোর জন্য যতটুকু দরকার, শুধু ততটুকু কোড। পরিষ্কার: টেস্ট সবুজ রেখে কোড সুন্দর করুন। তারপর পরের ছোট টেস্ট। TDD সব কাজে মানানসই নয় — অজানা কিছু নিয়ে পরীক্ষামূলক কোডে আগে খেলে দেখাই ভালো। কিন্তু যেখানে নিয়ম স্পষ্ট (যেমন ছাড় বা নম্বর থেকে গ্রেড), সেখানে এটি ছোট, পরিষ্কার নকশার দিকে ঠেলে দেয়।

উদাহরণের বাইরে — property-based testing আর fuzzing

উপরের সব টেস্ট নির্দিষ্ট উদাহরণ: “এই ইনপুটে এই ফল।” Property-based testing অন্যভাবে ভাবে: এমন একটি নিয়ম লিখুন যা সব ইনপুটে সত্য, আর টুল হাজারো ইনপুট বানিয়ে নিয়মটি ভাঙার চেষ্টা করুক। যেমন: “ছাড়ের পরের দাম কখনো আসল দামের বেশি নয়”, “parse_money(format_money(x)) == x”, “sort করার পরে প্রতিটি উপাদান পরেরটির চেয়ে ছোট বা সমান।” Haskell-এর QuickCheck এই ধারণার জনক; C++-এ RapidCheck-এর মতো library আছে, আর Catch2-এর generator দিয়েও কাছাকাছি কাজ করা যায়। ভাঙার মতো ইনপুট পেলে ভালো টুল সেটিকে ছোট করে সবচেয়ে সরল উদাহরণ দেখায়।

Fuzzing পর্ব ৪৩-এ দেখেছেন: অদ্ভুত, এলোমেলো ইনপুট দিয়ে কোড চালানো, সাধারণত sanitizer-এর সাথে, যাতে crash বা UB ধরা পড়ে। Property-testing প্রশ্ন করে “ফল কি ঠিক?”, fuzzing প্রশ্ন করে “কোনো ইনপুটে কি প্রোগ্রাম ভাঙে?” parse_money-এর মতো parser দুটোরই আদর্শ লক্ষ্য।

CTest আর CI — টেস্টকে অভ্যাসে পরিণত করা

পর্ব ৪৭-এ CMake দিয়ে প্রজেক্ট build করতে শিখেছেন। CMake-এর সঙ্গী CTest সব টেস্ট-প্রোগ্রাম চালায় আর ফল সাজিয়ে দেখায়। নিয়ম একটাই: যে প্রোগ্রাম 0 ফেরত দেয় সে পাস, অন্য কিছু ফেরত দিলে ফেল — ঠিক যেমন আমাদের harness করে।

# CMakeLists.txt-এ
enable_testing()
add_executable(shop_tests tests/shop_tests.cpp)
target_link_libraries(shop_tests PRIVATE shop GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(shop_tests)   # প্রতিটি TEST আলাদা CTest টেস্ট

# টার্মিনালে
cmake -S . -B build && cmake --build build
ctest --test-dir build --output-on-failure -j 8
ctest --test-dir build -R Discount   # নাম মিলিয়ে কিছু টেস্ট

নিজের harness হলে gtest_discover_tests-এর জায়গায় শুধু add_test(NAME tiny COMMAND tiny_tests). এরপর আসে CI: GitHub Actions, GitLab CI-এর মতো সার্ভিস প্রতিটি push বা pull request-এ নতুন মেশিনে build করে ctest চালায়। টেস্ট লাল হলে পরিবর্তনটি মূল শাখায় ঢোকে না। পর্ব ৪৩-এর পরামর্শ মতো ভালো CI কয়েকটি build চালায়: warning-কে error বানিয়ে একবার, sanitizer দিয়ে একবার, আর সম্ভব হলে GCC, Clang ও MSVC তিনটিতেই।

Code coverage — কোন লাইন চলেছে, তার মানচিত্র

Code coverage জানায় টেস্ট চলার সময় কোডের কোন লাইন বা শাখা অন্তত একবার চলেছে। GCC-তে --coverage দিয়ে compile করে টেস্ট চালান, তারপর gcov (বা তার ওপর বানানো gcovr, lcov) রিপোর্ট দেয়। Clang-এ একই কাজ -fprofile-instr-generate -fcoverage-mapping, তারপর llvm-profdata আর llvm-cov.

g++ -std=c++23 --coverage -O0 shop.cpp shop_tests.cpp -o shop_tests
./shop_tests
gcov shop.cpp          # shop.cpp.gcov: প্রতিটি লাইন কতবার চলেছে
# #####: মানে লাইনটি একবারও চলেনি

Coverage কী বলে না। ১০০% coverage মানে প্রতিটি লাইন চলেছে, ঠিক ফল দিয়েছে এমন নয়। যে টেস্ট discounted(100'000) ডাকে কিন্তু ফল যাচাই করে না, সেটিও পুরো coverage দেয়। আবার একটি লাইন চলেছে, কিন্তু সব branch (যেমন &&-এর দুই পাশ) চলেনি — line coverage সেটি লুকিয়ে রাখে। তাই coverage-কে লক্ষ্য না বানিয়ে মানচিত্র হিসেবে ব্যবহার করুন: “কোন জায়গায় কোনো টেস্টই পৌঁছায়নি?” সেই প্রশ্নের উত্তরেই এর আসল মূল্য।

পরীক্ষা ও ইন্টারভিউতে

প্র: unit test আর integration test-এর পার্থক্য কী? উ: unit test একটি অংশকে বাইরের সবকিছু ছাড়া পরীক্ষা করে; integration test কয়েকটি আসল অংশ একসাথে।

প্র: regression test কী? উ: একবার ঠিক করা বাগ যাতে আবার না ফেরে, সেজন্য ঠিক সেই অবস্থার জন্য লেখা টেস্ট।

প্র: GoogleTest-এ EXPECT_EQ আর ASSERT_EQ-এর পার্থক্য? উ: EXPECT ব্যর্থ হলে টেস্ট চলতে থাকে; ASSERT ব্যর্থ হলে সেই টেস্ট ফাংশন সেখানেই থামে।

প্র: ১০০% coverage কি বাগহীনতার প্রমাণ? উ: না; শুধু প্রমাণ করে প্রতিটি লাইন চলেছে, ফল যাচাই হয়েছে কি না তা নয়।

প্র: breakpoint আর watchpoint-এর পার্থক্য? উ: breakpoint একটি লাইনে থামায়; watchpoint থামায় যখন কোনো ভ্যারিয়েবলের মান বদলায়।

৭ · ডিবাগিং — বৈজ্ঞানিক পদ্ধতি, error পড়া আর ঠিকভাবে print করা

অনেকে ডিবাগিং করেন এলোমেলো: এখানে একটু বদলাই, ওখানে একটি print দিই, আবার চালাই, “হয়তো এবার হবে।” এটি অন্ধকারে ঢিল ছোড়া। অভিজ্ঞ প্রোগ্রামাররা অন্য কাজ করেন — বিজ্ঞানের পদ্ধতি। প্রথমে বাগটি নিয়মিত ঘটানো যায় কি না দেখেন; তারপর একটি স্পষ্ট অনুমান, এমন একটি পরীক্ষা যা অনুমানটিকে মিথ্যা প্রমাণ করতে পারে, আর ফল দেখে সন্দেহের পরিসর অর্ধেক করা। একটি দোকানের রাতের রিপোর্ট দিয়ে দেখুন:

  1. পর্যবেক্ষণcrashকখন, কোথায়?
  2. অনুমানকেন?একটি স্পষ্ট কারণ
  3. পরীক্ষাসত্য না মিথ্যা?অনুমান যাচাই
  4. ছোট করান্যূনতম ইনপুটপরিসর অর্ধেক
  5. সমাধানটেস্ট + ঠিকআর ফিরবে না
পর্যবেক্ষণতথ্য, মতামত নয়
nightly_report: Floating point exception (core dumped)
ঘটে: সপ্তাহে ১–২ রাত, সব রাতে নয়
একই দিনের ডেটা আবার চালালে: আবার crash  ← নিয়মিত ঘটানো গেল
অনুমান ১যা প্রথম মনে আসে
“বড় শাখার বিক্রি বেশি, যোগফল overflow করছে”
ভবিষ্যদ্বাণী: শুধু সবচেয়ে বড় শাখা দিলে crash হবে
পরীক্ষা ১অনুমান বাতিল
./nightly_report --only Mirpur     # সবচেয়ে বড় শাখা
OK  ← crash নেই: অনুমান ১ ভুল
ভুল প্রমাণিত অনুমানও তথ্য — একটি সম্ভাবনা বাদ গেল
অনুমান ২ ও পরীক্ষা ২নতুন সূত্র
crash-এর রাতগুলোর মিল: সেদিন একটি শাখা বন্ধ ছিল (হরতাল/ছুটি)
“যে শাখায় কোনো বিক্রি নেই, সেখানে গড় হিসাবে শূন্য দিয়ে ভাগ”
./nightly_report --only Uttara     # সেদিন বন্ধ
Floating point exception  ← অনুমান ২ টিকে গেল
ছোট করাminimal reproducible example
std::vector<int> sales{};      // ৫০ হাজার লাইনের ফাইল নয় — শুধু খালি
average(sales);              // এখানেই crash
পুরো রিপোর্ট বাদ, শুধু একটি ফাংশন, একটি ইনপুট
সমাধানআগে লাল টেস্ট, তারপর ঠিক
CHECK(!average({}).has_value());   // আগে লিখলাম: লাল
std::optional<int> average(const std::vector<int>& v) {
    if (v.empty()) return std::nullopt;
    ...                               // এখন সবুজ
  1. পর্যবেক্ষণ — বাগকে নিয়মিত ঘটান। প্রথম কাজ মতামত নয়, তথ্য: ঠিক কোন বার্তা, কোন রাতে, কোন ইনপুটে। সবচেয়ে বড় সাফল্য হলো বাগটিকে ইচ্ছামতো ঘটাতে পারা — একই দিনের ডেটা আবার চালালে আবার crash. যে বাগ নিয়মিত ঘটানো যায়, তার অর্ধেক কাজ শেষ।
  2. অনুমান — যাচাইযোগ্য একটি কারণ। “কিছু একটা ভুল” কোনো অনুমান নয়। ভালো অনুমান একটি ভবিষ্যদ্বাণী করে: “এটি সত্য হলে, শুধু বড় শাখা দিলে crash হবে।” তাহলেই পরীক্ষা করা যায়।
  3. পরীক্ষা — অনুমানকে ভুল প্রমাণের চেষ্টা। বড় শাখা একা চালালে crash হয় না, তাই overflow-এর অনুমান বাতিল। এটি ব্যর্থতা নয়: একটি সম্ভাবনা নিশ্চিতভাবে বাদ গেল। অনুমান যাচাই না করে “ঠিক” করলে আসল বাগ থেকেই যেত।
  4. নতুন সূত্র, নতুন অনুমান। crash-এর রাতগুলো মিলিয়ে দেখা গেল, প্রতিবার কোনো একটি শাখা বন্ধ ছিল — কোনো বিক্রি নেই। নতুন অনুমান: খালি তালিকায় গড় করতে গিয়ে শূন্য দিয়ে ভাগ। int-এ শূন্য দিয়ে ভাগ UB; x86 Linux-এ সাধারণত SIGFPE signal আসে, যার বার্তা বিভ্রান্তিকরভাবে “Floating point exception”, যদিও কোনো দশমিক সংখ্যাই নেই।
  5. পরিসর ছোট করা — ন্যূনতম উদাহরণ। পঞ্চাশ হাজার লাইনের ফাইল থেকে নেমে আসুন একটি খালি vector-এ। এর নাম minimal reproducible example — যত ছোট, তত দ্রুত কারণ দেখা যায়, আর অন্যকে (বা AI-কে) জিজ্ঞেস করাও তত সহজ।
  6. সমাধান — আগে লাল টেস্ট। ঠিক করার আগে খালি ইনপুটের টেস্টটি লিখুন আর লাল দেখুন — এতে প্রমাণ হয় টেস্টটি বাগটিকে সত্যিই ধরে। তারপর কোডে সিদ্ধান্ত: খালি তালিকার গড় “নেই”, তাই std::optional. টেস্ট সবুজ, আর এই বাগ এখন চিরকাল পাহারায়।

Compiler error — ওপর থেকে পড়ুন, প্রথমটি আগে

সব বাগ চলার সময় আসে না; অনেক সময় compiler একসাথে চল্লিশটি error ছুড়ে দেয়। ভয় পাবেন না, আর শেষ থেকে পড়বেন না। নিয়ম: শুধু প্রথম error-টি পড়ুন, ঠিক করুন, আবার compile করুন। একটি ভুল প্রায়ই পরের অনেকগুলো “ভুয়া” error জন্ম দেয়, কারণ compiler প্রথম ভুলের পরে বাকি কোড ভুলভাবে বোঝে।

shop.cpp:4:2: error: expected ';' after struct definition
shop.cpp:9:5: error: 'Item' was not declared in this scope
shop.cpp:12:9: error: 'items' was not declared in this scope
... আরও ৩৭টি

এখানে struct-এর শেষে একটি ; নেই। সেটি দিলেই বাকি সব উধাও। Template-এর error আরও লম্বা হয় (পর্ব ২৮, ৪০) — শত লাইনের মাঝে নিজের ফাইলের নাম আর required from here লেখা লাইনটি খুঁজুন; সেখানেই আপনার ডাকা জায়গা। Concept ব্যবহার করলে বার্তা অনেক ছোট আর পরিষ্কার হয় — এটিও concept-এর একটি বড় সুবিধা।

assert, exception নাকি logging?

তিনটিই “কিছু ভুল হয়েছে” জানায়, কিন্তু তিনটি আলাদা প্রশ্নের উত্তর দেয়:

assert

“এটি কখনো ঘটা উচিত নয় — ঘটলে প্রোগ্রামারের ভুল।” যেমন একটি private ফাংশনে index সবসময় সীমার ভেতরে থাকার কথা। ডেভেলপমেন্টে জোরে থামে; NDEBUG-এ উধাও। C++26-এ গৃহীত contracts (pre, post, contract_assert) এই ভূমিকাটিকেই ভাষার অংশ করছে।

ভেতরের নিয়ম পাহারা

exception / expected

“এটি ঘটতে পারে — বাইরের দুনিয়া ভুল দিয়েছে।” ব্যবহারকারী "abc" লিখলেন, ফাইল নেই, নেটওয়ার্ক বন্ধ। প্রোগ্রাম সামলে নেবে (পর্ব ৩৭)। এগুলো release-এও থাকে।

প্রত্যাশিত ব্যর্থতা

logging

“কী ঘটছে, পরে যাতে জানা যায়।” আসল ব্যবহারকারীর মেশিনে debugger থাকে না; সময়, অনুরোধ, সিদ্ধান্ত — log ফাইলে লেখা থাকলে crash-এর পরে গল্পটি পড়া যায়। প্রোগ্রামের আচরণ বদলায় না।

পরে পড়ার ডায়েরি

Print দিয়ে ডিবাগিং — ঠিকভাবে

Print debugging-কে অনেকে ছোট করে দেখেন, অথচ ঠিকভাবে করলে এটি খুব শক্তিশালী, বিশেষ করে যেখানে debugger চালানো কঠিন (thread, সার্ভার, contest). তিনটি নিয়ম: stderr-এ লিখুন (std::cerr বা std::println(stderr, ...)), যাতে আসল আউটপুটের সাথে না মেশে আর buffer-এ আটকে না থাকে; প্রসঙ্গ দিন — শুধু "here" নয়, কোন ফাংশন, কোন লাইন, কোন মান; আর কাজ শেষে সরিয়ে ফেলুন বা একটি debug সুইচের পেছনে রাখুন।

void trace(std::string_view msg,
           std::source_location loc = std::source_location::current()) {
    std::println(stderr, "[{}:{}] {}", loc.file_name(), loc.line(), msg);
}
// ব্যবহার:  trace(std::format("n={} sum={}", n, sum));
// আউটপুট:  [report.cpp:8] n=0 sum=0

৮ · Debugger — চলন্ত প্রোগ্রামকে থামিয়ে প্রশ্ন করা

Print দিয়ে আপনি আগে থেকে ঠিক করেন কী দেখবেন। Debugger উল্টো সুবিধা দেয়: প্রোগ্রাম যেকোনো জায়গায় থামিয়ে তখন যা খুশি দেখা — যেকোনো ভ্যারিয়েবল, কে কাকে ডেকেছে, এক লাইন করে এগোনো। Linux-এ GDB, macOS-এ LLDB, Windows-এ Visual Studio-র debugger; VS Code আর CLion ভেতরে এগুলোকেই ব্যবহার করে। মূল ধারণাগুলো সবগুলোতে এক:

কাজGDBVisual Studioমানে
Breakpointbreak file:lineF9এই লাইনে পৌঁছালে থামা
Conditional breakpointbreak 9 if n == 0breakpoint-এ ডান ক্লিক → Conditionsশর্ত সত্য হলেই শুধু থামা
Step overnextF10পরের লাইন; ফাংশন ডাক পুরোটা এক ধাপে
Step intostepF11ডাকা ফাংশনের ভেতরে ঢোকা
Step outfinishShift+F11এই ফাংশন শেষ করে ডাকার জায়গায় ফেরা
Watchwatch totalWatch window, data breakpointমান বদলালেই থামা / মান চোখে রাখা
BacktracebtCall Stack windowএখানে কীভাবে এলাম — ডাকার ক্রম

Debugger-কে আপনার কোড চেনাতে -g দিয়ে compile করুন (debug info), আর অপটিমাইজেশন বন্ধ বা হালকা রাখুন (-O0 বা -Og). -O2-এ ভ্যারিয়েবল প্রায়ই <optimized out> দেখায় আর লাইনগুলো এলোমেলো লাফায়। এবার অংশ ৭-এর খালি-শাখার বাগটি GDB দিয়ে ধরা যাক। নিচের transcript সরল করা, চালানোর জন্য নয়:

5int average(const std::vector<int>& v) {6    int sum = 0;7    for (int x : v) sum += x;8    int n = static_cast<int>(v.size());9    return sum / n;10}14    std::println("{}: {}", name, average(v));21    print_section("Section C", {});9    if (v.empty()) return std::nullopt;
GDB কী বলছে
কমান্ড
জানলাম
  1. Break — ফাঁদ পাতা। -g -O0 দিয়ে build করে GDB খুলি। সাধারণ breakpoint দিলে প্রতিটি শাখায় থামত — বিরক্তিকর। তাই conditional breakpoint: লাইন ৯-এ থামা, শুধু যখন n == 0. শত শত সঠিক ডাকের মাঝে সন্দেহজনকটিতেই থামা যায়।
  2. Run — প্রোগ্রাম চালানো। Section A আর B স্বাভাবিকভাবে ছাপা হলো, কারণ শর্ত মিথ্যা ছিল। তারপর GDB থামল ঠিক বিপজ্জনক লাইনে, ভাগ করার আগে. breakpoint ছাড়া চালালে GDB নিজে থেকেই থামত SIGFPE signal-এ (“Program received signal SIGFPE, Arithmetic exception”), কিন্তু তখন ভাগটি ঘটে গেছে।
  3. bt — কীভাবে এখানে এলাম। Backtrace call stack-এর প্রতিটি frame দেখায়: #0 এখনকার ফাংশন, #1 তাকে যে ডেকেছে, এভাবে main পর্যন্ত। দেখা গেল ডাকটি এসেছে “Section C” থেকে, যার তালিকা খালি। frame 1 লিখলে সেই স্তরে গিয়ে সেখানকার ভ্যারিয়েবলও দেখা যায়।
  4. print — প্রমাণ। print n বলে 0, আর GDB-র pretty printer v-কে সুন্দর করে দেখায়: “std::vector of length 0”. অনুমান এখন প্রমাণিত সত্য। info locals সব local ভ্যারিয়েবল একসাথে দেখায়, আর watch sum দিলে sum বদলানোর প্রতিটি মুহূর্তে থামত।
  5. Fix — টেস্টসহ সমাধান। GDB বাগ দেখায়, ঠিক করে না। খালি তালিকায় গড় “নেই”, তাই std::optional<int> ফেরত, শুরুতে v.empty() যাচাই, আর একটি নতুন টেস্ট average_of_empty_is_nullopt. ctest সবুজ — সেশন শেষ।

Crash হয়ে গেছে, debugger ছিল না — core dump

রাতে সার্ভারে crash হলে কেউ GDB নিয়ে বসে ছিল না। তখন কাজে লাগে core dump: crash-এর মুহূর্তে প্রসেসের মেমরির একটি ছবি, ফাইলে সেভ করা। Linux-এ ulimit -c unlimited দিয়ে এটি চালু করতে হয়, আর systemd-যুক্ত অনেক distribution-এ coredumpctl এগুলো জমা রাখে। পরে gdb ./report core খুলে bt লিখলেই crash-এর মুহূর্তের call stack — যেন সময় থামিয়ে ঘটনাস্থলে ফিরে যাওয়া। Windows-এ এর সমতুল্য minidump, যা Visual Studio খুলতে পারে। Build-এ -g রাখা (বা debug info আলাদা ফাইলে রেখে দেওয়া) জরুরি, নইলে stack-এ শুধু ঠিকানা দেখা যায়, নাম নয়।

Sanitizer — এক অনুচ্ছেদে মনে করিয়ে দেওয়া

পর্ব ৪৩-এ যা বিস্তারিত দেখেছেন, এখানে শুধু ডিবাগিংয়ের দৃষ্টিতে: debugger দেখায় crash কোথায় হলো, কিন্তু মেমরির ভুল প্রায়ই অনেক আগে ঘটে, crash হয় পরে অন্য জায়গায়। AddressSanitizer আর UBSan ভুলটিকে ঘটার মুহূর্তেই ধরে, তাই সব টেস্ট -fsanitize=address,undefined -g দিয়ে অন্তত একবার চালানো উচিত। Thread-এর বাগে (পর্ব ৪৫, ৪৬) আলাদা build-এ ThreadSanitizer. সন্দেহজনক crash-এ প্রথম প্রশ্ন: “sanitizer দিয়ে চালিয়েছি?”

৯ · বাগ শিকারের আরও তিন অস্ত্র — git bisect, রাবার হাঁস আর আগে ব্যর্থ টেস্ট

git bisect — কোন commit বাগটি আনল?

গত মাসের release-এ গ্রেড ঠিক আসত, আজ আসে না। মাঝে ২০০টি commit. প্রতিটি পড়ে দেখা অসম্ভব। git bisect এখানে binary search চালায়: আপনি একটি ভালো আর একটি খারাপ commit বলেন, git মাঝামাঝি একটি commit বের করে দেয়, আপনি টেস্ট চালিয়ে জানান ভালো না খারাপ, আর প্রতিবার সন্দেহের পরিসর অর্ধেক হয়। ২০০টি commit-এ মাত্র প্রায় ৮টি পরীক্ষা, ১০০০টিতে প্রায় ১০টি। ছোট একটি উদাহরণে দেখুন:

1c1  v1.4 release (good)2c2  add bKash payment3c3  receipt layout4c4  fix typo5c5  section report6c6  update README7c7  faster sort8c8  add Bangla font9c9  tidy includes10c10 refactor grade ladder11c11 new menu12c12 logging13c13 CI cache14c14 dark theme15c15 docs16c16 HEAD (bad)
সন্দেহের পরিসর
সন্দেহজনক
পরীক্ষা
ফল
  1. শুরু — দুই প্রান্ত চিহ্নিত। HEAD (c16) খারাপ, পুরোনো release c1 ভালো। মাঝের ১৫টির যেকোনোটি দোষী হতে পারে। git মাঝামাঝি c8 checkout করে দেয়; আমরা build করে গ্রেডের টেস্ট চালাই — পাস। তাই git bisect good, আর c2–c8 সবাই নির্দোষ।
  2. অর্ধেক বাদ। সন্দেহ এখন c9–c16. git দেয় c12; টেস্ট ব্যর্থ — git bisect bad. এক পরীক্ষায় c13–c16-ও বাদ, কারণ দোষী c12 বা তার আগে।
  3. আবার অর্ধেক। c9–c12 থেকে git দেয় c10; টেস্ট ব্যর্থ। দোষী c9 অথবা c10.
  4. শেষ পরীক্ষা। c9-এ টেস্ট পাস। তাহলে প্রথম খারাপ commit অবশ্যই c10.
  5. দোষী পাওয়া গেল — ৪টি পরীক্ষায়। “refactor grade ladder” — কেউ গ্রেডের শর্ত সাজাতে গিয়ে >=-কে > বানিয়েছে। এখন শুধু সেই একটি ছোট diff পড়লেই চলবে। git bisect reset দিয়ে আগের অবস্থায় ফিরুন। আর যদি একটি script থাকে যা ভালো হলে 0 আর খারাপ হলে 1 ফেরত দেয়, তবে git bisect run ./check.sh পুরো খোঁজটি নিজেই করে (125 ফেরত দিলে সেই commit বাদ দিয়ে এগোয়)।

আগে ব্যর্থ টেস্ট — off-by-one ধরা

বাগের রিপোর্ট এল: “৮০ নম্বর পাওয়া শিক্ষার্থী A+ পাচ্ছে না, A পাচ্ছে।” এটি একটি ক্লাসিক off-by-one: সীমানার নম্বরটি ভুল দিকে পড়েছে। ঠিক করার আগে আমরা এমন টেস্ট লিখি যা বাগটি দেখায়। নিচের প্রোগ্রামে একই টেস্ট-তালিকা পুরোনো (v1) আর ঠিক করা (v2) দুই সংস্করণে চলে — ইচ্ছা করেই v1-এর একটি টেস্ট ব্যর্থ হবে:

#include <print>
#include <string_view>

struct Case { int marks; std::string_view want; };
constexpr Case cases[]{{100, "A+"}, {80, "A+"}, {79, "A"},
                       {70, "A"}, {33, "pass"}, {32, "F"}};

std::string_view grade_v1(int m) {     // bug report: "80 shows A"
    return m > 80 ? "A+" : m >= 70 ? "A" : m >= 33 ? "pass" : "F";
}
std::string_view grade_v2(int m) {     // fixed: boundary included
    return m >= 80 ? "A+" : m >= 70 ? "A" : m >= 33 ? "pass" : "F";
}
void run(std::string_view name, auto grade) {
    int fails = 0;
    for (auto [m, want] : cases)
        if (auto got = grade(m); got != want) {
            ++fails;
            std::println("{}: FAIL marks {} got {} want {}", name, m, got, want);
        }
    std::println("{}: {} of {} failed", name, fails, std::size(cases));
}
int main() { run("v1", grade_v1); run("v2", grade_v2); }

আউটপুট তিন লাইন: v1: FAIL marks 80 got A want A+, তারপর v1: 1 of 6 failed, আর শেষে v2: 0 of 6 failed. প্রথম দুটি লাইন ইচ্ছাকৃত ব্যর্থতা — এটিই প্রমাণ যে টেস্টটি বাগটিকে সত্যিই ধরতে পারে। ঠিক করার আগে এই লালটি না দেখলে আপনি জানতেন না টেস্টটি কাজের, নাকি সবসময়ই সবুজ থাকে। টেস্ট-তালিকাটি লক্ষ করুন: প্রতিটি সীমানার মান, আর বেশিরভাগের ঠিক নিচের মানও (৮০ ও ৭৯, ৭০, ৩৩ ও ৩২)। একটি তালিকা আর একটি লুপ — একে বলে table-driven test; নতুন কেস যোগ করতে শুধু এক জোড়া মান লিখলেই হয়। বাস্তবে grade_v1 মুছে দেওয়া হয়, আর টেস্ট-তালিকাটি regression test হিসেবে CTest-এ চিরকাল থাকে। (এখানে গ্রেডের সিঁড়ি ছোট করা; বাংলাদেশের SSC/HSC-র পুরো সিঁড়িতে A+ ৮০–১০০, A ৭০–৭৯, এভাবে F ০–৩২ পর্যন্ত।)

রাবার হাঁসকে বুঝিয়ে বলুন

শুনতে হাস্যকর, কিন্তু কাজ করে: rubber duck debugging মানে টেবিলের একটি খেলনা হাঁসকে (বা যেকোনো বন্ধুকে, যাঁর প্রোগ্রামিং জানার দরকার নেই) কোডটি লাইন ধরে ধরে জোরে বুঝিয়ে বলা — “এই লাইনে n হলো ছাত্রসংখ্যা, এটা কখনো শূন্য হয় না, কারণ… ও আচ্ছা, হয়।” নামটি এসেছে Andrew Hunt আর David Thomas-এর The Pragmatic Programmer (১৯৯৯) বই থেকে। কারণটি মানসিক: মাথায় আমরা ধাপ লাফিয়ে যাই, কিন্তু মুখে বললে প্রতিটি অনুমান স্পষ্ট করতে হয়, আর ভুল অনুমানটি তখন নিজেই চোখে পড়ে। প্রশ্ন লিখতে গিয়েও একই ঘটনা ঘটে — অনেকে Stack Overflow-তে প্রশ্ন লিখতে লিখতেই উত্তর পেয়ে যান।

নিয়মিত ঘটান · একটি অনুমান · ভুল প্রমাণের চেষ্টা · পরিসর অর্ধেক · আগে লাল টেস্ট, তারপর সমাধান

১০ · AI-এর যুগে — AI-এর লেখা টেস্ট আর AI দিয়ে ডিবাগিং

AI টুলকে “এই ফাংশনের টেস্ট লেখো” বললে কয়েক সেকেন্ডে বিশটি টেস্ট আসে, সবই সবুজ। ঠিক এই “সবই সবুজ”-ই সন্দেহের কারণ। AI কোড দেখে টেস্ট লেখে, নিয়ম দেখে নয়; তাই সে প্রায়ই কোডের বর্তমান আচরণকেই “সঠিক” ধরে নেয় — বাগসহ। যে ভুলগুলো বারবার দেখা যায়:

AI যা করেসমস্যাযা হওয়া উচিত
শুধু happy path: "12.50", "100"খালি, অবৈধ, সীমানা, বিশাল ইনপুট বাদঅংশ ৫-এর ছয় ঘর ধরে কেস চাওয়া
কোড চালিয়ে যা আসে তাই প্রত্যাশিত মান হিসেবে লেখাgrade(80) == "A" — বাগটিকেই প্রতিশ্রুতি বানিয়ে আটকে দেয়প্রত্যাশিত মান আসবে নিয়ম থেকে, হাতে হিসাব করে
ভেতরের খুঁটিনাটি পরীক্ষা: private ফাংশন কতবার ডাকা হলো, ভেতরের vector-এর capacityrefactoring করলেই টেস্ট ভাঙে, যদিও আচরণ ঠিকশুধু public আচরণ যাচাই
প্রত্যাশিত মান একই সূত্রে হিসাব: EXPECT_EQ(f(x), x - x/10)কোডের ভুল সূত্র টেস্টেও কপি হয়নির্দিষ্ট, স্বাধীনভাবে জানা মান
Assert ছাড়া টেস্ট, বা EXPECT_TRUE(true)কখনো ব্যর্থ হতে পারে নাকোড ইচ্ছা করে ভেঙে দেখুন, টেস্ট লাল হয় কি না
now(), rand(), sleep ব্যবহারflaky — কখনো পাস, কখনো ফেলঘড়ি ও seed inject করা

AI-এর শক্তি অন্য জায়গায়: সে এমন edge case মনে করিয়ে দিতে পারে যা আপনি ভাবেননি, আর লম্বা stack trace বা template error-কে সহজ ভাষায় ব্যাখ্যা করতে পারে। তাকে কোড লেখক নয়, প্রশ্নকর্তা হিসেবে ব্যবহার করুন:

নিচের prompt-টি কপি করে নিজের ফাংশন আর (থাকলে) crash-এর আউটপুটসহ দিন:

তুমি একজন অভিজ্ঞ C++ টেস্ট ইঞ্জিনিয়ার। নিচের ফাংশনের নিয়মটি আমি লিখে দিচ্ছি; কোড নয়, নিয়ম অনুযায়ী টেস্ট ভাবো। ১. edge case-এর একটি তালিকা দাও: খালি, এক, অনেক, সীমানার দুই পাশ, অবৈধ, বিশাল ইনপুট। প্রতিটির জন্য প্রত্যাশিত ফল নিয়ম থেকে বলো; নিয়মে না থাকলে "অস্পষ্ট" লিখে আমাকে প্রশ্ন করো। ২. কোন কেসগুলোতে বর্তমান কোড ভুল করতে পারে বলে মনে হয়, কারণসহ বলো। ৩. টেস্টগুলো GoogleTest-এ Arrange-Act-Assert ছকে লেখো। প্রত্যাশিত মান কোড চালিয়ে বের করবে না। ৪. নিচের stack trace বা error থাকলে #0 থেকে পড়ে বলো: কোথায় ঘটেছে, কোন frame আমার কোডের, আর সম্ভাব্য কারণ কী। নিশ্চিত না হলে "সন্দেহ" লিখো। নিয়ম: [এখানে নিয়ম] কোড: [এখানে কোড] আউটপুট: [এখানে stack trace]

AI-এর টেস্ট আর ব্যাখ্যা যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১GoogleTest, JUnit, NUnit — প্রায় সব test framework দেখতে এক রকম কেন? Kent Beck-এর SUnit থেকে TDD পর্যন্ত এই “xUnit” পরিবারের গল্পটি কী?

গল্পের কেন্দ্রে Kent Beck, একজন মার্কিন সফটওয়্যার প্রকৌশলী, যিনি ১৯৯০-এর দশকে Smalltalk ভাষায় কাজ করতেন। সেই দশকের মাঝামাঝি তিনি “Simple Smalltalk Testing: With Patterns” নামে একটি লেখায় একটি ছোট টেস্টিং কাঠামো বর্ণনা করেন, যা পরে SUnit নামে পরিচিত হয়। এর ধারণাগুলো আজ আপনার কাছে চেনা লাগবে: প্রতিটি টেস্ট একটি ছোট পদ্ধতি; একটি fixture, যার setUp প্রতিটি টেস্টের আগে নতুন অবস্থা বানায় আর tearDown পরে পরিষ্কার করে; টেস্টগুলোকে একটি suite-এ জোড়া; আর ফল তিন রকম — পাস, প্রত্যাশিত ব্যর্থতা, অপ্রত্যাশিত error.

প্রচলিত গল্প অনুযায়ী ১৯৯৭ সালে একটি বিমানযাত্রায় Beck আর Erich Gamma (Design Patterns বইয়ের চার লেখকের একজন) মিলে এই নকশা Java-তে লেখেন — সেটিই JUnit. Java তখন দ্রুত ছড়াচ্ছিল, আর JUnit-এর সাথে ছড়াল এর ছক। এরপর প্রায় প্রতিটি ভাষায় একই কাঠামোর framework এল: C++-এ CppUnit, .NET-এ NUnit, Python-এর unittest. এই পুরো পরিবারকে একসাথে বলা হয় xUnit. ২০০৮ সালে Google ওপেন-সোর্স করে GoogleTest, যার TEST_F আর SetUp()/TearDown() সরাসরি এই বংশের। Catch2 আর doctest কিছুটা ভিন্ন পথে হাঁটে (SECTION, সাধারণ ==), তবু মূল ধারণা একই।

একই সময়ে Beck Extreme Programming (XP) নামে একটি কাজের পদ্ধতি প্রচার করেন (বই: Extreme Programming Explained, ১৯৯৯), যেখানে টেস্ট আগে লেখা একটি কেন্দ্রীয় অভ্যাস। ২০০২ সালে তাঁর Test-Driven Development: By Example বইয়ে red–green–refactor চক্রটি বিস্তারিতভাবে আসে। Beck নিজে বলেছেন তিনি TDD “আবিষ্কার” করেননি, বরং “পুনরাবিষ্কার” করেছেন — পুরোনো একটি প্রোগ্রামিং বইয়ে পড়েছিলেন, প্রত্যাশিত আউটপুট আগে লিখে তারপর প্রোগ্রামের আউটপুটের সাথে মেলানোর কথা। অর্থাৎ ধারণাটি পুরোনো; Beck-এর অবদান ছিল একে ছোট, পুনরাবৃত্ত, যন্ত্রে চালানো যায় এমন অভ্যাসে পরিণত করা।

এই বংশ বোঝার ব্যবহারিক লাভ আছে: একটি xUnit framework শিখলে বাকিগুলো কয়েক ঘণ্টায় শেখা যায়, কারণ fixture, assertion, suite, runner — শব্দগুলো সব জায়গায় একই অর্থে চলে।

মূল উপলব্ধি: test framework-এর আসল উদ্ভাবন কোনো জটিল প্রযুক্তি নয়, একটি সরল ছক — প্রতিটি টেস্ট আলাদা, নতুন অবস্থায় শুরু, স্বয়ংক্রিয়ভাবে চালানো। সেই ছকটি এত কাজের যে প্রায় তিন দশক ধরে প্রায় সব ভাষা এটিই নকল করেছে।

প্র ০২Edsger Dijkstra ১৯৭০ সালে লিখেছিলেন, “Program testing can be used to show the presence of bugs, but never to show their absence.” তাহলে টেস্ট লেখার মানে কী? আর টেস্টের বাইরে “প্রমাণ” কীভাবে পাওয়া যায়?

Edsger W. Dijkstra ছিলেন নেদারল্যান্ডসের কম্পিউটার বিজ্ঞানী, structured programming-এর অন্যতম প্রবক্তা, আর ১৯৭২ সালের Turing Award বিজয়ী। কথাটি তাঁর ১৯৭০ সালের “Notes on Structured Programming” লেখায় আছে, আর এর কাছাকাছি কথা তিনি ১৯৬৯ সালের NATO সফটওয়্যার ইঞ্জিনিয়ারিং সম্মেলনেও বলেছিলেন। যুক্তিটি সরল গণিত: দুটি ৩২-বিট int নেয় এমন একটি ফাংশনের সম্ভাব্য ইনপুট প্রায় ১৮ কুইন্টিলিয়ন (২-এর ৬৪ ঘাত) জোড়া। আপনার বিশটি টেস্ট এর একটি অতি ক্ষুদ্র নমুনা। একটি টেস্ট ব্যর্থ হলে বাগ আছে প্রমাণ হয়; সব পাস করলে শুধু এটুকু প্রমাণ হয় যে ওই বিশটি ইনপুটে বাগ নেই।

Dijkstra-র সমাধান ছিল উল্টো দিক থেকে: প্রোগ্রাম এমনভাবে গঠন করা যাতে তার সঠিকতা যুক্তি দিয়ে দেখানো যায় — loop invariant, precondition-postcondition, ছোট ছোট প্রমাণযোগ্য ধাপ। এই ধারা আজ formal verification নামে বড় হয়েছে। বাস্তবে এর উদাহরণ আছে: seL4 নামের একটি microkernel-এর জন্য গাণিতিক প্রমাণ তৈরি হয়েছে যে তার C কোড তার স্পেসিফিকেশন মানে; Xavier Leroy-র নেতৃত্বে তৈরি CompCert একটি C compiler, যার অনুবাদের সঠিকতা যন্ত্রে যাচাই করা প্রমাণ দিয়ে দেখানো। কিন্তু এগুলোর খরচ বিশাল — প্রমাণ প্রায়ই কোডের চেয়ে কয়েক গুণ বড়, আর বিশেষজ্ঞ লাগে।

তাহলে সাধারণ প্রোগ্রামারের জন্য শিক্ষা কী? প্রথমত, টেস্ট বাছাই করুন বুদ্ধি দিয়ে — অংশ ৫-এর ছয় ঘর এলোমেলো নমুনা নয়, বাগ যেখানে থাকার সম্ভাবনা বেশি সেই জায়গা। দ্বিতীয়ত, নমুনার আকার বাড়ান যন্ত্র দিয়ে — property-based testing আর fuzzing লাখো ইনপুট চালায়। তৃতীয়ত, কিছু জিনিস প্রমাণ করুন compiler দিয়ে — টাইপ, static_assert, concept, constexpr-এ UB ধরা (পর্ব ৩৯) — এগুলো ছোট আকারের formal প্রমাণ, প্রতিটি compile-এ বিনামূল্যে। চতুর্থত, sanitizer আর static analyzer যোগ করুন। কোনোটি একা যথেষ্ট নয়, কিন্তু একসাথে আত্মবিশ্বাস অনেক বাড়ায়।

মূল উপলব্ধি: টেস্ট প্রমাণ নয়, প্রমাণের নমুনা। তাই প্রশ্নটি “সব টেস্ট পাস করেছে?” নয়, “আমার টেস্টগুলো কি ঠিক সেই জায়গাগুলো দেখছে যেখানে বাগ লুকাতে পারে?”

প্র ০৩১৯৪৭ সালে Harvard Mark II কম্পিউটারের ভেতরে একটি সত্যিকারের মথ পাওয়া গিয়েছিল, আর বলা হয় সেখান থেকেই “bug” শব্দটি এসেছে। গল্পটির কতটুকু সত্য?

ঘটনাটি সত্য, কিন্তু প্রচলিত ব্যাখ্যাটি পুরোপুরি নয়। ১৯৪৭ সালের সেপ্টেম্বরে (লগবইয়ে তারিখ ৯ সেপ্টেম্বর) Harvard বিশ্ববিদ্যালয়ে Mark II নামের electromechanical কম্পিউটারটি ভুল আচরণ করছিল। যন্ত্রটি চলত হাজারো relay — ছোট বৈদ্যুতিক সুইচ — দিয়ে। পরিচালকরা খুঁজে দেখলেন একটি relay-র ভেতরে একটি মথ আটকে আছে। তাঁরা পোকাটিকে সাবধানে বের করে লগবইয়ের পাতায় টেপ দিয়ে সেঁটে দেন, আর পাশে লেখেন: “First actual case of bug being found.” সেই পাতাটি আজ Smithsonian-এর National Museum of American History-র সংগ্রহে আছে।

দুটি সংশোধন জরুরি। প্রথমত, মথটি সম্ভবত Grace Hopper নিজে খুঁজে পাননি; তিনি সেই দলের সদস্য ছিলেন, আর পরে বহু বছর বক্তৃতায় গল্পটি বলে এটিকে বিখ্যাত করেন। Hopper নিজে কম্পিউটিংয়ের ইতিহাসে অনেক বড় নাম — প্রথম দিকের compiler-এর কাজ, আর COBOL ভাষার পেছনের চিন্তায় তাঁর বড় ভূমিকা। দ্বিতীয়ত, এবং আরও গুরুত্বপূর্ণ: লেখাটির রসিকতা বুঝুন — “actual case of bug”. অর্থাৎ “bug” শব্দটি তখনই যান্ত্রিক ত্রুটি বোঝাতে চালু ছিল, আর এবার আক্ষরিক অর্থেই একটি পোকা পাওয়া গেল। প্রকৌশলে শব্দটির ব্যবহার আরও পুরোনো; যেমন ১৮৭০-এর দশকে Thomas Edison-এর চিঠিতে আবিষ্কারের ছোটখাটো সমস্যা বোঝাতে “bugs” শব্দটি পাওয়া যায়।

এই গল্প থেকে প্রকৌশলের একটি সত্যিকারের শিক্ষা আছে: লগবই। Mark II-এর দল প্রতিটি ঘটনা সময়সহ লিখে রাখত — কী ঘটল, কী পরীক্ষা করা হলো, কী পাওয়া গেল। এটিই অংশ ৭-এর পর্যবেক্ষণ ধাপ, আর এটিই আজকের logging আর bug tracker-এর পূর্বপুরুষ। কঠিন বাগে অভিজ্ঞ প্রোগ্রামাররা আজও একটি নোট খোলা রাখেন: কোন অনুমান, কোন পরীক্ষা, কী ফল — যাতে একই পরীক্ষা দুবার না হয়, আর বাতিল অনুমান আবার ফিরে না আসে।

আরেকটি মজার মিল: সেই বাগটি ছিল একটি “পরিবেশের” সমস্যা, কোডের নয়। আজও অনেক বাগ কোডে থাকে না — compiler-এর সংস্করণ, ভিন্ন locale, ভিন্ন সময়-অঞ্চল, ভিন্ন CPU. তাই পর্যবেক্ষণে সবসময় পরিবেশটিও লিখে রাখুন।

মূল উপলব্ধি: ইতিহাসের সবচেয়ে বিখ্যাত বাগ থেকে শেখার জিনিস পোকাটি নয়, লগবইটি। ডিবাগিং শুরু হয় সৎ, লিখিত পর্যবেক্ষণ দিয়ে।

প্র ০৪Coverage বলে কোন লাইন চলেছে, কিন্তু টেস্ট ভুল ধরতে পারে কি না বলে না। “Mutation testing” নামের কৌশল কীভাবে টেস্টেরই টেস্ট নেয়, আর এর সীমা কোথায়?

ধারণাটি এই: যদি আপনার টেস্ট সত্যিই ভালো হয়, তবে কোডে ছোট একটি ভুল ঢোকালে অন্তত একটি টেস্ট লাল হওয়া উচিত। Mutation testing এই কাজটি যন্ত্র দিয়ে হাজারবার করে। একটি টুল আপনার কোডের একটি করে ছোট পরিবর্তিত কপি বানায় — যাকে বলে mutant: >= বদলে >, + বদলে -, true বদলে false, একটি লাইন মুছে দেওয়া, return x বদলে return 0. প্রতিটি mutant-এ পুরো টেস্ট-সেট চলে। কোনো টেস্ট লাল হলে mutant “মারা গেছে” — ভালো খবর। সব সবুজ থাকলে mutant “বেঁচে আছে” — অর্থাৎ কোডে এই ভুল থাকলেও আপনার টেস্ট টের পেত না। মারা যাওয়া mutant-এর শতাংশই mutation score.

ধারণাটি পুরোনো: ১৯৭০-এর দশকের শুরুতে Richard Lipton এর প্রস্তাব দেন, আর ১৯৭৮ সালে DeMillo, Lipton ও Sayward-এর “Hints on Test Data Selection” প্রবন্ধে এটি বিস্তারিত আসে। এর পেছনে দুটি অনুমান: “competent programmer” — প্রোগ্রামাররা সাধারণত প্রায়-সঠিক কোড লেখেন, ভুলগুলো ছোট; আর “coupling effect” — ছোট ভুল ধরতে পারা টেস্ট সাধারণত বড়, জটিল ভুলও ধরে। এই পর্বের grade-এর বাগ ঠিক একটি “>= থেকে >” mutant!

দীর্ঘদিন এটি গবেষণাগারে আটকে ছিল, কারণ খরচ বিশাল — হাজার mutant মানে টেস্ট-সেট হাজারবার চালানো। আধুনিক টুল এটি কমিয়েছে: শুধু পরিবর্তিত কোডে mutant বানানো, শুধু সেই লাইন ছোঁয় এমন টেস্ট চালানো। C ও C++-এর জন্য LLVM-ভিত্তিক Mull-এর মতো টুল আছে; Java-তে PIT, Python-এ mutmut বহুল ব্যবহৃত। Google-এর প্রকৌশলীরা ২০১৮ সালে একটি গবেষণাপত্রে জানান, তারা code review-এর সময় পরিবর্তিত লাইনে বেছে বেছে কয়েকটি বেঁচে থাকা mutant দেখান, আর ডেভেলপাররা সেগুলো দেখে প্রায়ই নতুন টেস্ট লেখেন।

সীমাও আছে। কিছু mutant “equivalent” — পরিবর্তনের পরেও প্রোগ্রামের আচরণ একই (যেমন যে লুপ কখনো সীমানায় পৌঁছায় না, তার < বদলে !=)। এগুলো কোনো টেস্টই মারতে পারে না, আর যন্ত্র সবসময় চিনতে পারে না। তাই ১০০% mutation score লক্ষ্য নয়; বেঁচে থাকা mutant-গুলো পড়ে দেখাই আসল কাজ।

মূল উপলব্ধি: coverage প্রশ্ন করে “টেস্ট কি এখানে গিয়েছিল?”, mutation testing প্রশ্ন করে “গিয়ে কি কিছু দেখেছিল?” হাতে-কলমে এর ছোট রূপ আপনি আজই করতে পারেন: একটি শর্ত ইচ্ছা করে ভেঙে দেখুন, কোনো টেস্ট লাল হয় কি না।

প্র ০৫“Flaky test” — যে টেস্ট কোড না বদলালেও কখনো পাস করে, কখনো ফেল। কেন এটি একটি সাধারণ ব্যর্থ টেস্টের চেয়েও ক্ষতিকর, আর এর সাধারণ কারণগুলো কী?

একটি ব্যর্থ টেস্ট একটি স্পষ্ট বার্তা দেয়: “কিছু ভেঙেছে।” একটি flaky test দেয় শব্দ, বার্তা নয়। প্রথমবার লাল হলে দল খুঁজে দেখে; দশমবার দেখে, “ও, ওটা তো মাঝে মাঝে এমন করে” — আর আবার চালিয়ে সবুজ পেয়ে এগিয়ে যায়। ধীরে ধীরে পুরো দলের মনে লাল রঙের অর্থ হারিয়ে যায়। তারপর একদিন সত্যিকারের বাগে টেস্ট লাল হয়, আর কেউ বিশ্বাস করে না। Google-এর টেস্টিং ব্লগে ২০১৬ সালে জানানো হয়েছিল, তাদের বিশাল টেস্ট-ব্যবস্থায় প্রায় ১.৫% টেস্ট-রানে flaky ফল আসত, আর প্রায় ১৬% টেস্টে কোনো না কোনো মাত্রায় flakiness ছিল — অর্থাৎ বড় প্রতিষ্ঠানেও এটি ছোট সমস্যা নয়।

কারণগুলো কয়েকটি চেনা দলে পড়ে। সময়: আসল ঘড়ি পড়া — মাঝরাত পেরোলে তারিখ বদলায়, মাসের শেষ দিন, সময়-অঞ্চল, বা CI মেশিন ধীর বলে “২০০ ms-এর মধ্যে শেষ হবে” শর্ত ভাঙা। Thread: data race (পর্ব ৪৫), বা “sleep(100ms) দিলেই অন্য thread কাজ শেষ করবে” ধরে নেওয়া — ব্যস্ত মেশিনে সেটি মিথ্যা। ক্রম: এক টেস্ট একটি global, static ভ্যারিয়েবল বা ফাইল বদলায়, পরের টেস্ট তার ওপর নির্ভর করে; ক্রম বদলালে বা সমান্তরালে চালালে ভাঙে। Randomness: seed ছাড়া random. বাইরের জগৎ: নেটওয়ার্ক, আসল ডেটাবেস, খালি port. আর C++-এর বিশেষ কারণ: UB — মান না দেওয়া ভ্যারিয়েবল বা সীমার বাইরে পড়া, যার ফল মেমরিতে আগে কী ছিল তার ওপর নির্ভর করে।

সমাধানও এই পর্বেই আছে: ঘড়ি আর seed inject করা (অংশ ৫), প্রতিটি টেস্টে নতুন fixture, global অবস্থা নয়, অপেক্ষার জন্য sleep নয় বরং condition variable বা future, আর sanitizer. ধরার যন্ত্রও আছে: GoogleTest-এ --gtest_shuffle ক্রম এলোমেলো করে আর --gtest_repeat=100 একই টেস্ট বারবার চালায়; CTest-এ --repeat until-fail:50. ঠিক করতে সময় লাগলে টেস্টটিকে সাময়িক “quarantine”-এ রাখুন — আলাদা তালিকায়, মালিক আর সময়সীমাসহ — কিন্তু চুপচাপ মুছে দেবেন না।

মূল উপলব্ধি: টেস্টের মূল্য তার বিশ্বাসযোগ্যতায়। যে টেস্টকে কেউ বিশ্বাস করে না, সেটি না থাকার চেয়েও খারাপ, কারণ সে আসল সতর্কবার্তাকেও শব্দে ডুবিয়ে দেয়। Deterministic টেস্ট নকশার প্রশ্ন, ভাগ্যের নয়।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. Edge case সাজান: একটি ফাংশন split_bill(total, people) রেস্টুরেন্টের বিল কয়েকজনে ভাগ করে, পয়সায়। অংশ ৫-এর ছয় ঘরের প্রতিটির জন্য একটি করে ইনপুট লিখুন, আর বলুন কোনটিতে আগে থেকে একটি সিদ্ধান্ত নিতে হবে।

    খালি: people = 0 — শূন্য দিয়ে ভাগ; আগে ঠিক করতে হবে exception নাকি std::expected. এক: people = 1 — পুরো বিল একজনের। অনেক: people = 7, total = 1000 — ভাগ মেলে না (১৪২ × ৭ = ৯৯৪); বাকি ৬ পয়সা কে দেবে, এটিও একটি নিয়মের সিদ্ধান্ত। সীমানা: total = 0, আর total ঠিক people-এর গুণিতক। অবৈধ: ঋণাত্মক total বা people. বিশাল: এমন total যা int-এ ধরে না। এখানে দুটি সিদ্ধান্ত লাগে — শূন্য জন, আর ভাগশেষ।

  2. EXPECT না ASSERT: একটি GoogleTest টেস্টে প্রথমে একটি ফাংশন std::vector ফেরত দেয়, তারপর আপনি তার মাপ ৩ কি না আর তিনটি উপাদানের মান যাচাই করবেন। কোন লাইনে কোনটি ব্যবহার করবেন, কেন?

    মাপের লাইনে ASSERT_EQ(v.size(), 3u) — মাপ ভুল হলে পরের v[2] সীমার বাইরে পড়বে (UB), তাই টেস্ট সেখানেই থামা দরকার। তিনটি মানের জন্য EXPECT_EQ — এগুলো একে অপরের ওপর নির্ভর করে না, আর একবারে সবগুলো ভুল দেখতে পাওয়া ভালো। বিকল্প: পুরো vector একবারে তুলনা — EXPECT_EQ(v, (std::vector{1, 2, 3})); GoogleTest তখন দুটি vector-ই ছাপিয়ে দেখায়।

  3. Bisect-এর হিসাব: শেষ ভালো release আর আজকের মধ্যে ৫০০টি commit. প্রতিটি build আর টেস্টে ৪ মিনিট লাগে। git bisect দিয়ে প্রায় কত সময়ে দোষী commit পাবেন? আর কোন অবস্থায় bisect বিভ্রান্ত হয়?

    ২-এর ৯ ঘাত ৫১২, তাই প্রায় ৯টি পরীক্ষা — মোটামুটি ৩৬ মিনিট। এক এক করে দেখলে সবচেয়ে খারাপ ক্ষেত্রে ৫০০ × ৪ = ২০০০ মিনিট, ৩৩ ঘণ্টার বেশি। Bisect বিভ্রান্ত হয় যখন টেস্টটি flaky (একই commit কখনো ভালো, কখনো খারাপ), যখন মাঝের কিছু commit build-ই হয় না (তখন git bisect skip বা script থেকে 125), আর যখন বাগটি দুটি আলাদা commit মিলে তৈরি। তাই bisect-এর আগে একটি নির্ভরযোগ্য, deterministic টেস্ট দরকার।

  4. নিজে লিখুন (কোড): অংশ ৩-এর tiny_tests.cpp-এ এমন একটি টেস্ট যোগ করুন যা প্রমাণ করে “ছাড়ের পরের দাম কখনো আসল দামের বেশি নয়” — ০ থেকে ২০০০০০ পয়সা পর্যন্ত প্রতি ৭ পয়সা অন্তর। এটি কোন ধরনের টেস্টের ছোট রূপ?

    একটি লুপ: for (long long p = 0; p <= 200'000; p += 7) CHECK(discounted(p) <= p);. চালালে passed-এর সংখ্যা অনেক বেড়ে যাবে (২৮,৫৭২টি নতুন পাস), কিন্তু failed শূন্যই থাকবে। এটি property-based testing-এর ছোট, হাতে বানানো রূপ: নির্দিষ্ট উদাহরণের বদলে একটি নিয়ম, অনেক ইনপুটে। আসল property-testing টুল ইনপুট নিজে বানায় আর ব্যর্থ ইনপুট ছোট করে দেখায়। লক্ষ করুন, নিয়মটি সীমানার বাগ (> বনাম >=) ধরবে না — তার জন্য অংশ ৩-এর নির্দিষ্ট উদাহরণই লাগে। দুই ধরনের টেস্ট একে অপরের পরিপূরক।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি বাগ খুঁজে ঠিক করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. বাগটি নিয়মিত ঘটানোর উপায় বের করা
  2. একটি যাচাইযোগ্য অনুমান লেখা
  3. অনুমানকে ভুল প্রমাণ করতে পারে এমন পরীক্ষা চালানো
  4. ইনপুট ছোট করে ন্যূনতম উদাহরণে পৌঁছানো
  5. বাগটি ধরে এমন একটি ব্যর্থ টেস্ট লেখা
  6. কোড ঠিক করে টেস্ট সবুজ হতে দেখা

GoogleTest-এ EXPECT_EQ আর ASSERT_EQ-এর মূল পার্থক্য কী?

  • ASSERT_EQ শুধু int তুলনা করে, EXPECT_EQ যেকোনো টাইপ
  • ASSERT_EQ ব্যর্থ হলে সেই টেস্ট থামে, EXPECT_EQ চালিয়ে যায়
  • ASSERT_EQ release build-এ বাদ পড়ে
  • EXPECT_EQ ব্যর্থ হলে পুরো প্রোগ্রাম বন্ধ হয়

Testing pyramid অনুযায়ী একটি প্রজেক্টে কোন ধরনের টেস্ট সবচেয়ে বেশি সংখ্যায় থাকা উচিত?

  • Unit test
  • Integration test
  • End-to-end test
  • হাতে চালিয়ে দেখা পরীক্ষা

একটি ভালো আর একটি খারাপ commit-এর মাঝে ১০২৪টি commit আছে। git bisect দিয়ে প্রথম খারাপ commit পেতে মোটামুটি কয়টি পরীক্ষা লাগবে?

  • প্রায় ১০২৪টি
  • প্রায় ৫১২টি
  • প্রায় ১০টি
  • ঠিক ১টি

একটি C++ প্রজেক্টের টেস্ট ১০০% line coverage দেয়। এর থেকে নিশ্চিতভাবে কী বলা যায়?

  • প্রোগ্রামে কোনো বাগ নেই
  • প্রতিটি if-এর সব শাখা পরীক্ষা হয়েছে
  • টেস্টগুলো যেকোনো ছোট ভুল ধরতে পারবে
  • টেস্ট চলার সময় প্রতিটি লাইন অন্তত একবার চলেছে

নিচের কোনটি একটি টেস্টকে flaky (কোড না বদলালেও কখনো পাস, কখনো ফেল) বানানোর সবচেয়ে সম্ভাব্য কারণ?

  • টেস্টে নির্দিষ্ট, হাতে লেখা ইনপুট ব্যবহার
  • যাচাই করা ফাংশন ভেতরে system_clock::now() পড়ে
  • প্রতিটি টেস্টের জন্য নতুন fixture object
  • Random engine-এ একটি নির্দিষ্ট seed দেওয়া

<cassert>-এর assert দিয়ে লেখা টেস্ট release build-এ কিছুই যাচাই না করার কারণ কী হতে পারে?

  • -O2 সব শর্ত মুছে দেয়
  • release-এ stdout বন্ধ থাকে
  • build-এ NDEBUG সংজ্ঞায়িত, তাই assert কিছুই করে না
  • C++23-এ assert বাতিল হয়েছে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. টেস্ট চালানো যায় এমন প্রতিশ্রুতি — regression আটকায়, refactoring নিরাপদ করে, আর নিজেই যাচাই হওয়া ডকুমেন্টেশন।
  2. পিরামিড আর AAA: অনেক unit, কিছু integration, অল্প end-to-end; প্রতিটি টেস্টে Arrange, একটি Act, স্বাধীনভাবে জানা মান দিয়ে Assert.
  3. নকশাই টেস্টকে সহজ করে: pure function, interface ও fake, ঘড়ি ও seed inject; আর ছয় ঘরের edge case — খালি, এক, অনেক, সীমানা, অবৈধ, বিশাল।
  4. ডিবাগিং বিজ্ঞান: নিয়মিত ঘটান, অনুমান করুন, ভুল প্রমাণের চেষ্টা করুন, পরিসর অর্ধেক করুন। প্রথম compiler error আগে; GDB-তে break, run, bt, print; git bisect-এ দোষী commit.
  5. আগে লাল টেস্ট, তারপর সমাধান — আর AI-এর টেস্টে প্রত্যাশিত মান নিয়ম থেকে আসে কি না, নিজে যাচাই করুন।

পরের পর্বে: কোড এখন সঠিক, আর তার প্রমাণও আছে। এবার প্রশ্ন — যথেষ্ট দ্রুত কি? অনুমান নয়, মেপে দেখে দ্রুত করা: profiler, benchmark, আর CPU-র cache কীভাবে আপনার কোডের গতি ঠিক করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান