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

টেস্টিং — Vitest আর Playwright দিয়ে নিশ্চিন্তে কোড বদলানো

JavaScript Testing — Unit, Integration & End-to-End Tests, Assertions, Test Doubles, Fake Timers, Vitest, Testing Library, Playwright, Flaky Tests, TDD & CI

This Bangla lesson shows how tests let you change code with confidence: unit, integration and end-to-end levels, the testing pyramid and trophy, Arrange-Act-Assert and assertions, testing pure functions and edge cases, test doubles with fake fetch, fake timers and dependency injection, Vitest, Testing Library, snapshots and coverage, Playwright, flaky tests, TDD and CI.

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

  • কেন টেস্ট লিখবেন, আর testing pyramid ও testing trophy কী বলে
  • একটি টেস্টের গঠন, assertion-এর ধরন, আর কোন input-এ টেস্ট করবেন (edge case)
  • Stub, mock, spy, fake — নকল fetch, নকল ঘড়ি আর dependency injection
  • Vitest, Testing Library, snapshot আর coverage — কোনটির কী সীমা
  • Playwright দিয়ে আসল ব্রাউজারে E2E, flaky টেস্ট সারানো, TDD আর CI

১ · টেস্ট কী, আর কেন — কয়েক সেকেন্ডের নিরাপত্তা জাল

ধরুন আপনি একটি অনলাইন দোকানের কার্ট বানিয়েছেন। মোট দাম, ১৫% VAT আর কুপন ছাড় — সব ঠিকঠাক চলছে। এক মাস পরে ছাড়ের নিয়ম একটু বদলাতে হলো। সাধারণ উপায়: অ্যাপ খুলে কয়েকটি পণ্য কার্টে দেওয়া, কুপন বসানো, মোট দেখা — পাঁচ মিনিট, আর যাচাই হলো হয়তো তিনটি অবস্থা। খালি কার্ট, ঋণাত্মক পরিমাণ, ১০০% ছাড় দেখাই হলো না।

স্বয়ংক্রিয় টেস্ট (automated test) এই কাজটি কোডকে দিয়ে করায়। একটি টেস্ট হলো ছোট একটি প্রোগ্রাম, যা আপনার কোডকে জানা input দিয়ে চালায় আর ফল প্রত্যাশিত মানের সাথে মেলায়। মিললে পাস, না মিললে ফেল — সাথে কারণ। একবার লিখলে টেস্ট হাজারবার চলে, প্রতিবার একই মনোযোগ নিয়ে, কয়েক মিলিসেকেন্ডে।

একটি গার্মেন্টস কারখানার কোয়ালিটি চেক ভাবুন। প্রতিটি শার্ট শিপমেন্টে যাওয়ার আগে একজন পরিদর্শক মাপের ফিতা দিয়ে মাপেন: কলার ১৫ ইঞ্চি, হাতা ২৪ ইঞ্চি, সেলাই সোজা। মাপগুলো আগে থেকেই লেখা — এটি প্রত্যাশিত মান. মাপ মিলল না? শার্টটি আলাদা হয়ে যায়, কারণসহ। সেলাই মেশিন বদলালেও একই চেক চলে। আপনার টেস্টগুলো সেই পরিদর্শক — শুধু ক্লান্ত হয় না, আর এক সেকেন্ডে হাজার শার্ট মাপে।

টেস্ট লেখার চারটি বড় কারণ আছে। প্রতিটি আলাদা সমস্যা সারায়:

🔁 Regression ধরা

নতুন বদল পুরোনো কোনো কাজ চুপচাপ ভেঙে দিলে তাকে বলে regression. টেস্ট সেটি কয়েক সেকেন্ডে ধরে — ব্যবহারকারী অভিযোগ করার আগেই।

🛠️ নির্ভয়ে refactor

আচরণ না বদলে কোডের গঠন পরিষ্কার করাকে বলে refactoring. টেস্ট না থাকলে প্রতিটি refactor একটি জুয়া। টেস্ট থাকলে বদলান, চালান, সবুজ দেখলে এগিয়ে যান।

📖 চলমান দলিল

it("১০০% ছাড়ে মোট ০, ঋণাত্মক নয়") — এমন নাম পড়েই নতুন সহকর্মী জানেন কোডের নিয়ম কী। আর পুরোনো মন্তব্যের মতো এই দলিল মিথ্যা হতে পারে না: ভুল হলে টেস্ট লাল হয়।

🐞 বাগ আটকে রাখা

বাগ রিপোর্ট এলে আগে এমন একটি টেস্ট লিখুন, যা বাগটি ঘটায় (ফেল করে)। তারপর ঠিক করুন। সেই বাগ আর কখনো ফিরে আসতে পারবে না — টেস্টটি পাহারায় থাকবে।

একটি ভুল ধারণা আগেই দূর করা যাক: টেস্ট প্রমাণ করে না যে কোডে বাগ নেই। টেস্ট শুধু দেখায়, যে অবস্থাগুলো আপনি ভেবেছেন, সেগুলোতে কোড ঠিক চলে। তাই কোন অবস্থাগুলো ভাববেন, সেটিই আসল দক্ষতা — অংশ ৪-এ। পর্ব ২১-এর খরচের খাতায় আপনি self-test লিখেছিলেন, আর পর্ব ২৯-এ console.assert দেখেছিলেন। এই পর্বে সেই ধারণাই পেশাদার রূপ পাবে: আলাদা টেস্ট ফাইল, একটি test runner, আর প্রতিটি push-এ স্বয়ংক্রিয়ভাবে চলা।

২ · টেস্টের তিন স্তর — pyramid, trophy আর static analysis

সব টেস্ট এক রকম নয়। কোনোটি একটি function একা দেখে, কোনোটি আসল ব্রাউজারে পুরো দোকান চালায়। মূল দ্বন্দ্ব হলো গতি বনাম বাস্তবতা: যত বাস্তব টেস্ট, তত ধীর আর তত ভঙ্গুর।

স্তরকী দেখেগতিউদাহরণ
Static analysisকোড না চালিয়েই টাইপ আর সন্দেহজনক নকশালেখার সাথে সাথেESLint (পর্ব ৪৬), TypeScript (পর্ব ৪৭)
Unitএকটি function বা ছোট module, একামিলিসেকেন্ডcartTotal([]) ০ দেয় কি না
Integrationকয়েকটি অংশ একসাথে — UI + state, API + ডেটাবেসদশ-শত মিলিসেকেন্ড“কার্টে যোগ করুন” চাপলে কার্টের সংখ্যা বাড়ে
End-to-end (E2E)আসল ব্রাউজার, আসল সার্ভার, পুরো যাত্রাসেকেন্ডপণ্য যোগ → চেকআউট → মোট ৳৮৪০

এই স্তরগুলো কতটুকু করে রাখবেন, তা নিয়ে দুটি জনপ্রিয় ছবি আছে। Testing pyramid: নিচে চওড়া ভিত্তি — অনেক unit টেস্ট; মাঝে কম integration; চূড়ায় অল্প কয়েকটি E2E. যুক্তি সহজ: সস্তা টেস্ট বেশি, দামি টেস্ট কম। Testing trophy একটু ভিন্ন কথা বলে: সবচেয়ে নিচে static analysis, আর সবচেয়ে মোটা অংশ integration — কারণ ফ্রন্টএন্ডে বেশিরভাগ বাগ লুকিয়ে থাকে অংশগুলোর জোড়ায়, একক function-এ নয়। দুটির ইতিহাস আর বিতর্ক ভাবনার প্রশ্ন ৩-এ।

মূল কথা

আকৃতি যা-ই হোক, নিয়ম একই: একটি আচরণ সবচেয়ে নিচের যে স্তরে নির্ভরযোগ্যভাবে যাচাই করা যায়, সেখানেই করুন। VAT-এর হিসাব unit টেস্টে; বোতাম চাপলে হিসাব পাতায় দেখা যায় কি না, integration-এ; আর “পুরো কেনাকাটা সত্যিই কাজ করে” — শুধু সেটির জন্য E2E.

৩ · একটি টেস্টের গঠন — Arrange, Act, Assert আর assertion

প্রায় প্রতিটি ভালো টেস্ট তিনটি অংশে লেখা যায়। একে বলে Arrange-Act-Assert (AAA):

  • Arrange (সাজানো): input, প্রাথমিক অবস্থা আর দরকারি নকল জিনিস তৈরি।
  • Act (করা): যা যাচাই করছেন, ঠিক সেই একটি কাজ — সাধারণত একটি function কল।
  • Assert (মেলানো): ফল প্রত্যাশিত মানের সাথে মেলানো। এটি করে একটি assertion — না মিললে ভুল ছোড়ে, আর runner সেই টেস্টকে ফেল ধরে।

Vitest বা Jest-এ টেস্ট দেখতে এমন। describe কয়েকটি টেস্টকে একটি দলে রাখে (test suite), আর it বা test (দুটি একই জিনিস) একটি টেস্ট। নাম লিখুন সাধারণ ভাষায়, একটি আচরণের বাক্য হিসেবে — যাতে ফেল করলে শুধু নাম পড়েই বোঝা যায় কী ভাঙল:

import { describe, it, expect } from "vitest";
import { subtotal, applyDiscount, withVat } from "./cart.js";

describe("cart", () => {
  it("খালি কার্টে subtotal ০", () => {
    expect(subtotal([])).toBe(0);
  });

  it("দাম × পরিমাণ যোগ করে (পয়সায়)", () => {
    const items = [{ price: 12000, qty: 2 }, { price: 60000, qty: 1 }];  // Arrange
    const result = subtotal(items);                                      // Act
    expect(result).toBe(84000);                                          // Assert — ৳৮৪০
  });

  it("১০% ছাড় আর ১৫% VAT", () => {
    expect(applyDiscount(84000, 10)).toBe(75600);
    expect(withVat(75600)).toBe(86940);
  });

  it("ঋণাত্মক qty দিলে RangeError", () => {
    expect(() => subtotal([{ price: 100, qty: -1 }])).toThrow(RangeError);
  });
});

লক্ষ করুন, দাম রাখা হয়েছে পয়সায় (৳১২০ = ১২০০০ পয়সা), পূর্ণসংখ্যা হিসেবে। পর্ব ১০-এ দেখেছেন, 0.1 + 0.2 ঠিক 0.3 নয় — টাকার হিসাবে দশমিক floating-point ব্যবহার করলে টেস্ট (আর হিসাব) এক পয়সার জন্য ভাঙতে পারে। টার্মিনালে npx vitest run চালালে প্রত্যাশিত আউটপুট মোটামুটি এমন (সময় আপনার মেশিনে আলাদা হবে):

 ✓ src/cart.test.js (4 tests) 3ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Duration  412ms

Assertion-এর ধরন — কোনটি কখন

Vitest / JestNode-এর node:assert/strictকী যাচাই করে
expect(x).toBe(y)assert.equal(x, y)হুবহু একই — Object.is দিয়ে। number, string-এ মান; object-এ একই reference
expect(x).toEqual(y)assert.deepEqual(x, y)ভেতরের গঠন আর মান একই — দুটি আলাদা object হলেও
expect(x).toStrictEqual(y)(strict mode-এ deepEqual-ই কঠোর)toEqual-এর মতো, তবে undefined property আর class-ও মেলায়
expect(() => f()).toThrow(RangeError)assert.throws(() => f(), RangeError)function ভুল ছোড়ে কি না
await expect(p).rejects.toThrow("HTTP 500")await assert.rejects(p, /HTTP 500/)Promise reject হয় কি না

সবচেয়ে বেশি ভুল হয় প্রথম দুটিতে। expect({ taka: 840 }).toBe({ taka: 840 }) ফেল করে — দুটি আলাদা object, দেখতে এক হলেও (পর্ব ১৪-এর reference). এবার একটি test framework ভেতরে কীভাবে কাজ করে, তা নিজে দেখুন — মাত্র কয়েক লাইনে test আর expect বানিয়ে। শেষ দুটি টেস্ট ইচ্ছা করে ভাঙা:

function test(name, fn) {                          // একটি টেস্ট = নাম + একটি function
  try { fn(); console.log("✓", name); }
  catch (e) { console.log("✗", name, "→", e.message); }
}
const show = JSON.stringify;
function expect(actual) {                          // assertion: না মিললে throw
  return {
    toBe(exp) { if (!Object.is(actual, exp)) throw new Error(`চেয়েছি ${show(exp)}, পেলাম ${show(actual)}`); },
    toEqual(exp) { if (show(actual) !== show(exp)) throw new Error(`চেয়েছি ${show(exp)}, পেলাম ${show(actual)}`); },
    toThrow() { try { actual(); } catch { return; } throw new Error("throw হওয়ার কথা ছিল, হয়নি"); },
  };
}
function cartTotal(items) {                        // যে কোড টেস্ট করছি
  if (items.some((it) => !(it.qty > 0))) throw new RangeError("qty ধনাত্মক হতে হবে");
  return items.reduce((sum, it) => sum + it.price * it.qty, 0);
}
test("খালি কার্টে মোট ০", () => expect(cartTotal([])).toBe(0));
test("২ × ১২০ + ১ × ৬০০ = ৮৪০", () =>
  expect(cartTotal([{ price: 120, qty: 2 }, { price: 600, qty: 1 }])).toBe(840));
test("ঋণাত্মক qty দিলে throw", () => expect(() => cartTotal([{ price: 50, qty: -1 }])).toThrow());
test("object-এ toEqual মেলে", () => expect({ taka: 840 }).toEqual({ taka: 840 }));
test("object-এ toBe ভাঙে", () => expect({ taka: 840 }).toBe({ taka: 840 }));
test("দশমিক টাকা: ০.১ × ৩", () => expect(cartTotal([{ price: 0.1, qty: 3 }])).toBe(0.3));

আউটপুটের প্রথম চার লাইনে ✓. পঞ্চম লাইন ✗ object-এ toBe ভাঙে → চেয়েছি {"taka":840}, পেলাম {"taka":840} — দেখতে হুবহু এক, তবু ফেল, কারণ Object.is দুটি আলাদা object-কে আলাদাই বলে। শেষ লাইন ✗ দশমিক টাকা: ০.১ × ৩ → চেয়েছি 0.3, পেলাম 0.30000000000000004 — floating-point-এর সেই পুরোনো ফাঁদ, এবার একটি টেস্ট তা ধরে ফেলল। পুরো যন্ত্রটি আসলে এটুকুই: assertion না মিললে throw করে, আর runner প্রতিটি টেস্ট try/catch-এ চালিয়ে ফল লেখে। আসল framework এর ওপর যোগ করে টেস্ট খোঁজা, async, hook আর সুন্দর diff. আর আমাদের JSON.stringify-এর তুলনা কাঁচা (key-এর ক্রম বা undefined-এ ভুল করে); আসল toEqual প্রতিটি property ঘুরে মেলায়।

একটি টেস্ট রান ভেতরে — সংগ্রহ, প্রস্তুতি, চালানো, রিপোর্ট

Runner আপনার টেস্ট ফাইল পড়ার সাথে সাথে টেস্ট চালায় না। প্রথমে সে শুধু তালিকা বানায়, তারপর একটি একটি করে চালায়। প্রতিটি টেস্টের আগে beforeEach নামের একটি hook চলে, যা নতুন করে প্রস্তুতি নেয় — যাতে একটি টেস্টের রেখে যাওয়া অবস্থা পরেরটিকে ছুঁতে না পারে। নিচের কার্টে একটি বাগ আছে: VAT যোগ করতে 1.15 দিয়ে গুণ করার কথা, ভুলে লেখা হয়েছে 1.5. ধাপে ধাপে দেখুন:

1describe("Cart", () => {2  let cart;3  beforeEach(() => {4    cart = new Cart();5  });6  it("একটি পণ্যে মোট ১২০", () => {7    cart.add({ price: 120, qty: 1 });8    expect(cart.total()).toBe(120);9  });10  it("৮৪০-এ ১৫% VAT যোগে ৯৬৬", () => {11    cart.add({ price: 120, qty: 2 });12    cart.add({ price: 600, qty: 1 });13    expect(cart.totalWithVat()).toBe(966);14  });15});
Runner-এর খাতা
পর্যায়
cart
প্রত্যাশা বনাম ফল
তালিকা
  1. সংগ্রহ। Runner ফাইলটি import করে। describe-এর ভেতরের function সাথে সাথে চলে, কিন্তু it(...) শুধু নাম আর function একটি তালিকায় জমা রাখে — টেস্টের শরীর এখনো চলেনি। এ কারণেই describe-এর ভেতরে সরাসরি কাজের কোড লেখা ভুল; সেটি সংগ্রহের সময়েই চলে যাবে।
  2. টেস্ট ১-এর আগে beforeEach. একটি নতুন, খালি Cart তৈরি হলো। এটিই Arrange-এর ভাগ করা অংশ — প্রতিটি টেস্ট একই পরিষ্কার অবস্থা থেকে শুরু করে।
  3. টেস্ট ১ — Act আর Assert. একটি পণ্য যোগ হলো, total() দিল ১২০. toBe(120) মিলল, কোনো ভুল ছোড়া হয়নি — runner একে পাস ধরল।
  4. টেস্ট ২-এর আগে আবার beforeEach. আগের কার্টটি ফেলে দিয়ে আবার নতুন কার্ট। যদি beforeEach না থাকত, টেস্ট ২ টেস্ট ১-এর পণ্যসহ শুরু করত, আর ফল নির্ভর করত কোন টেস্ট আগে চলল তার ওপর — flaky টেস্টের এক বড় উৎস (অংশ ৭)।
  5. টেস্ট ২ — ফেল আর রিপোর্ট। ৮৪০-এর ১৫% VAT ১২৬, তাই প্রত্যাশা ৯৬৬. কিন্তু বাগের কারণে ৮৪০ × ১.৫ = ১২৬০. Assertion ভুল ছুড়ল, runner সেটি ধরে টেস্টের নাম, প্রত্যাশা আর আসল ফল দেখাল, তারপর পরের টেস্টে গেল — একটি ফেলে পুরো রান থামে না। শেষে সারাংশ, আর process-এর exit code হয় 1, যাতে CI বুঝতে পারে কিছু ভেঙেছে।
C/C++/Python/Java জানলে

গঠন সব ভাষায় প্রায় একই, কারণ সবাই একই পূর্বপুরুষ থেকে এসেছে (ভাবনার প্রশ্ন ১)। Java-র JUnit 5-এ @Test, @BeforeEach আর assertEquals হুবহু it, beforeEach আর toBe-এর কাজ করে (Java-র JUnit টেস্টিং). Python-এর pytest সাধারণ assert লিখতে দেয় আর fixture দিয়ে প্রস্তুতি নেয় (Python-এর pytest). C++-এ GoogleTest-এর EXPECT_EQ বা Catch2-এর REQUIRE (C++-এর টেস্টিং ও ডিবাগিং), আর C-তে অনেকে নিজের CHECK macro লেখেন (C-তে টেস্টিং). JavaScript-এর বাড়তি দিক দুটি: প্রায় সব টেস্টে async আসে, আর DOM-এর জন্য একটি নকল ব্রাউজার পরিবেশ লাগে।

৪ · কী টেস্ট করবেন — pure function আগে, তারপর edge case

সবচেয়ে সহজে টেস্ট হয় pure function (পর্ব ২৩): একই input-এ সবসময় একই output, বাইরের কিছু বদলায় না। নেটওয়ার্ক নেই, ঘড়ি নেই, DOM নেই — শুধু input দিন, output মেলান। তাই পর্ব ২১-এর functional core, imperative shell নীতি টেস্টের জন্যও সোনা: হিসাবের সব যুক্তি pure function-এ রাখুন (subtotal, applyDiscount, withVat), আর DOM বা fetch থাকুক পাতলা একটি বাইরের খোলসে। Core-এ হাজারটি দ্রুত unit টেস্ট, খোলসে অল্প কয়েকটি integration টেস্ট।

Edge case — বাগ যেখানে লুকিয়ে থাকে

“২টি পণ্য, ৳৮৪০” — এমন সাধারণ উদাহরণ প্রায় সবসময় কাজ করে। বাগ থাকে সীমানায়, যাকে বলে edge case. একটি কার্ট মডিউলের জন্য একটি তালিকা বানান, আর প্রতিটির জন্য আগে ঠিক করুন সঠিক আচরণ কী:

ধরনInputপ্রত্যাশিত আচরণ (আপনার সিদ্ধান্ত)
খালি[]মোট ০, ভুল নয়
শূন্য / ঋণাত্মকqty: 0, qty: -1RangeError — ঋণাত্মক কার্ট মানে টাকা ফেরত দেওয়া!
ভগ্নাংশqty: 1.5ভুল — শার্ট দেড়টি হয় না (চাল কেজিতে হলে আলাদা নিয়ম)
সীমাছাড় ০%, ১০০%, ১০১%১০০%-এ মোট ০; ১০০-র বেশি হলে ভুল
গোল করা৳৯৯.৯৯-এ ১৫% VATপয়সায় হিসাব, শেষে একবার Math.round — নিয়মটি লিখে রাখুন
বাংলা input"০১৭১২৩৪৫৬৭৮", "৫"বাংলা অঙ্ক গ্রহণ করবেন? করলে আগে ইংরেজি অঙ্কে রূপান্তর
ভুল টাইপprice: "120"form থেকে আসা string — "120" * 2 চলে, কিন্তু "120" + 0 হয় "1200" (পর্ব ০৬)

শেষ কলামটি আসলে পণ্যের সিদ্ধান্ত — টেস্ট লেখা মানে স্পেসিফিকেশন লেখা, তাই প্রশ্নগুলো প্রথম ভুল অর্ডারের দিনের আগেই সামনে আসে।

একটি টেবিল, অনেক টেস্ট — বাংলাদেশি ফোন নম্বর

পর্ব ৩০-এ আপনি regex দিয়ে বাংলাদেশি মোবাইল নম্বর যাচাই করেছিলেন। এমন function-এ একই আকৃতির অনেক কেস থাকে। প্রতিটির জন্য আলাদা it না লিখে একটি টেবিল দিন — Vitest-এর it.each প্রতিটি সারির জন্য একটি আলাদা টেস্ট তৈরি করে:

import { describe, it, expect } from "vitest";
import { isBdPhone } from "./phone.js";
// phone.js: export const isBdPhone = (s) => /^(?:\+?880|0)1[3-9]\d{8}$/.test(s.replace(/[\s-]/g, ""));

describe("isBdPhone", () => {
  it.each([
    ["01712345678", true],
    ["+8801712345678", true],
    ["01712-345678", true],        // মাঝে হাইফেন — মানুষ এভাবেই লেখেন
    ["01212345678", false],        // 012 দিয়ে কোনো অপারেটর নেই
    ["0171234567", false],         // এক অঙ্ক কম
    ["০১৭১২৩৪৫৬৭৮", false],        // বাংলা অঙ্ক: \d শুধু 0-9 মেলায়
    ["", false],
  ])("%s → %s", (input, expected) => {
    expect(isBdPhone(input)).toBe(expected);
  });
});

প্রতিটি সারি রিপোর্টে আলাদা নামে দেখা যায় (01212345678 → false), তাই কোনটি ভাঙল সাথে সাথে বোঝা যায়। বাংলা অঙ্কের সারিটি একটি সিদ্ধান্তকে দলিল করছে: JavaScript regex-এ \d (u flag দিলেও) শুধু ASCII 0-9 মেলায়, তাই এখন বাংলা নম্বর গ্রহণ হয় না। বাংলাদেশি ব্যবহারকারীরা প্রায়ই বাংলা কিবোর্ডে নম্বর লেখেন, তাই আপনি হয়তো আগে অঙ্কগুলো রূপান্তর করতে চাইবেন। তখন এই সারিটি true হবে — আর টেস্টটিই আগে বদলাবেন, যা অংশ ৮-এর TDD-র ধারণা।

৫ · Test double — নকল fetch, নকল ঘড়ি আর dependency injection

বাস্তব কোড pure নয়। loadProducts() সার্ভার থেকে পণ্য আনে, debounced search ৩০০ ms অপেক্ষা করে, অর্ডারের পরে SMS যায়। এগুলো আসল অবস্থায় টেস্ট করলে তিনটি সমস্যা: ধীর (নেটওয়ার্কে সেকেন্ড লাগে), অনিশ্চিত (সার্ভার বন্ধ, ডেটা বদলে গেছে, load-shedding), আর ক্ষতিকর (প্রতিটি টেস্টে সত্যিকারের SMS!). সমাধান হলো আসল নির্ভরতার জায়গায় একটি নকল বসানো — যাকে সাধারণভাবে বলে test double, সিনেমার স্টান্ট ডাবলের মতো।

🧱 Stub

নির্দিষ্ট প্রশ্নে বাঁধা উত্তর দেয়: “fetch ডাকলে এই দুটি পণ্য দাও।” শুধু input জোগায়, কিছু মনে রাখে না। (stub)

🕵️ Spy

চুপচাপ খাতা রাখে: কতবার ডাকা হলো, কোন argument দিয়ে। পরে আপনি জিজ্ঞেস করেন, “search ঠিক একবার, ‘শাড়ি’ দিয়ে ডাকা হয়েছে তো?” (spy)

🎭 Mock

প্রত্যাশা আগেই জানা একটি নকল — কীভাবে ডাকা উচিত, আর না হলে টেস্ট ফেল। Vitest-এর vi.fn() একসাথে stub আর spy দুটোই, তাই দৈনন্দিন ভাষায় সবাই একে “mock” বলেন। (mock)

🧪 Fake

সত্যিকারের কাজ করে, কিন্তু সহজ উপায়ে: ডেটাবেসের বদলে একটি Map, আসল ঘড়ির বদলে একটি নকল ঘড়ি। (fake)

Dependency injection — নকল বসানোর দরজা

নকল বসাতে হলে কোডে একটি দরজা লাগে। যে function নিজের ভেতরে সরাসরি global fetch ডাকে, তার fetch বদলানো কঠিন। কিন্তু যদি fetch বাইরে থেকে দেওয়া হয় — loadProducts(fetchFn = fetch) — তাহলে টেস্টে একটি নকল দেওয়া যায়, আর অ্যাপে ডিফল্ট আসলটি চলে। নির্ভরতা ভেতরে তৈরি না করে বাইরে থেকে নেওয়ার এই নকশাকে বলে dependency injection. একই কৌশল ঘড়ির জন্য (now = new Date() parameter), random-এর জন্য, আর SMS পাঠানোর জন্য। নিচের ধাপগুলোতে দেখুন, নকল fetch কী বদলায়:

  1. যাচাই হচ্ছেloadProductsআপনার কোড
  2. আসল fetchinternet → serverধীর, বদলায়
  3. নকল fetchmock.fn()বাঁধা উত্তর + খাতা
  4. যাচাইassertফল + কল
অ্যাপেআসল নেটওয়ার্ক
async function loadProducts(fetchFn = fetch) {
  const res = await fetchFn("/api/products");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()).map((p) => p.name);
}
// অ্যাপে: loadProducts()  → ডিফল্ট, আসল fetch
আসল fetch দিয়ে টেস্ট করলেসমস্যা
✗ সার্ভার বন্ধ            → টেস্ট লাল, অথচ কোডে ভুল নেই
✗ নতুন পণ্য যোগ হলো       → প্রত্যাশিত তালিকা আর মেলে না
✗ ধীর নেটওয়ার্ক         → প্রতিটি টেস্টে ১–৩ সেকেন্ড
✗ 500 কীভাবে ঘটাব?       → ভুলের পথ টেস্টই করা যায় না
টেস্টে — Arrangeনকল বসানো
const fakeFetch = mock.fn(async () =>
  new Response('[{"name":"শাড়ি"},{"name":"লুঙ্গি"}]'));
// stub: সবসময় এই দুটি পণ্য
// spy:  প্রতিটি কল fakeFetch.mock.calls-এ জমা
Act আর ফলের Assertনেটওয়ার্ক ছাড়াই, মিলিসেকেন্ডে
const names = await loadProducts(fakeFetch);
assert.deepEqual(names, ["শাড়ি", "লুঙ্গি"]);   ✓
কলের Assertspy-এর খাতা পড়া
fakeFetch.mock.callCount()               // 1
fakeFetch.mock.calls[0].arguments[0]     // "/api/products"
// ভুলের পথ: async () => new Response("", { status: 500 })
//          → await assert.rejects(..., /HTTP 500/)  ✓
  1. আসল কোড। loadProducts fetch-কে একটি parameter হিসেবে নেয়, ডিফল্ট মান আসল fetch. অ্যাপের কোনো কলে কিছুই বদলায় না — loadProducts() আগের মতোই চলে। শুধু একটি দরজা খোলা রইল।
  2. আসল নেটওয়ার্কে টেস্টের সমস্যা। টেস্ট লাল হলে বোঝা যায় না: কোড ভাঙল, না ঢাকার কোনো সার্ভার বন্ধ? আর সবচেয়ে জরুরি পথ — সার্ভার 500 দিলে কী হয় — ইচ্ছামতো ঘটানোই যায় না।
  3. নকল বসানো। mock.fn() একটি function বানায়, যা ডাকলে একটি বাঁধা Response দেয় (stub), আর প্রতিটি কল খাতায় লিখে রাখে (spy). আসল Response class ব্যবহার করায় res.ok আর res.json() হুবহু আসলের মতো আচরণ করে।
  4. Act আর ফলের Assert. loadProducts(fakeFetch) নেটওয়ার্ক না ছুঁয়েই দুটি নাম ফেরত দিল। টেস্ট এখন প্রতিবার একই ফল দেবে, সার্ভারের অবস্থা যা-ই হোক।
  5. কলের Assert — পরিমিত মাত্রায়। Spy-এর খাতা থেকে দেখা যায় fetch ঠিক একবার, সঠিক URL দিয়ে ডাকা হয়েছে। কিন্তু সাবধান: শুধু কল যাচাই করা টেস্ট আসলে কোড কী ফেরত দেয় তা দেখে না। প্রধান assertion সবসময় ফল বা দৃশ্যমান আচরণের ওপর; কল যাচাই সহায়ক মাত্র।

Fake timers — ৩০০ ms অপেক্ষা না করে debounce যাচাই

পর্ব ৩৬-এর debounce টেস্ট করতে চাইলে সত্যিই ৩০০ ms অপেক্ষা করবেন? একশোটি টেস্টে ৩০ সেকেন্ড, আর ব্যস্ত CI মেশিনে timer একটু দেরি করলে টেস্ট মাঝে মাঝে ফেল। Fake timers আসল setTimeout-এর জায়গায় একটি নকল ঘড়ি বসায়, যা শুধু টেস্ট নিজে এগিয়ে দিলে এগোয়: “এবার ২৯৯ ms এগোও” — আর সেই সময়ের মধ্যে বাজার কথা এমন সব timer সাথে সাথে চলে যায়।

Node.js 20-এর ভেতরেই একটি test runner আছে — node:test মডিউল, Node 20-এ stable — সাথে mock.fn() আর mock.timers. নিচের কোডে সেগুলো আর node:assert/strict দিয়ে debounce আর নকল fetch-এর তিনটি টেস্ট। node --test চালালে আসল runner প্রতিটি টেস্টের সময়সহ লম্বা রিপোর্ট দেয়, যা প্রতিবার একটু আলাদা আসে। তাই এখানে আউটপুট স্থির রাখতে একটি তিন লাইনের check() runner ব্যবহার করা হয়েছে:

const { mock } = require("node:test");               // Node 20-এর built-in টেস্ট টুল
const assert = require("node:assert/strict");
process.removeAllListeners("warning");               // MockTimers এখনো "experimental" — সতর্কবার্তা লুকাই
async function check(name, fn) {                     // ছোট্ট runner: পাস হলে ✓, ভাঙলে ✗
  try { await fn(); console.log("✓", name); } catch (e) { console.log("✗", name, "→", e.message); }
}
const debounce = (fn, wait) => { let id; return (...a) => { clearTimeout(id); id = setTimeout(() => fn(...a), wait); }; };
async function loadProducts(fetchFn) {               // fetch বাইরে থেকে আসে — dependency injection
  const res = await fetchFn("/api/products");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()).map((p) => p.name);
}
(async () => {
  await check("debounce: টাইপ থামার ৩০০ ms পরে একবারই খোঁজে", () => {
    mock.timers.enable({ apis: ["setTimeout"] });    // আসল ঘড়ির বদলে নকল ঘড়ি
    const search = mock.fn();                        // spy: কতবার, কী দিয়ে ডাকা হলো
    const onType = debounce(search, 300);
    onType("শা"); onType("শাড়"); onType("শাড়ি");
    mock.timers.tick(299); assert.equal(search.mock.callCount(), 0);
    mock.timers.tick(1);   assert.deepEqual(search.mock.calls.map((c) => c.arguments), [["শাড়ি"]]);
    mock.timers.reset();
  });
  await check("loadProducts: নকল fetch থেকে পণ্যের নাম আনে", async () => {
    const fakeFetch = mock.fn(async () => new Response('[{"name":"শাড়ি"},{"name":"লুঙ্গি"}]'));
    assert.deepEqual(await loadProducts(fakeFetch), ["শাড়ি", "লুঙ্গি"]);
    assert.equal(fakeFetch.mock.calls[0].arguments[0], "/api/products");
  });
  await check("loadProducts: সার্ভার 500 দিলে reject করে", () =>
    assert.rejects(loadProducts(async () => new Response("", { status: 500 })), /HTTP 500/));
})();

আউটপুট তিনটি ✓ লাইন: ✓ debounce: টাইপ থামার ৩০০ ms পরে একবারই খোঁজে, ✓ loadProducts: নকল fetch থেকে পণ্যের নাম আনে আর ✓ loadProducts: সার্ভার 500 দিলে reject করে. প্রথম টেস্টে তিনটি দ্রুত টাইপের পরে নকল ঘড়ি ২৯৯ ms এগোল — search একবারও চলেনি; আরও ১ ms এগোতেই ঠিক একবার চলল, শুধু শেষ লেখা “শাড়ি” নিয়ে। আসল সময় লাগল প্রায় শূন্য। mock.timers.reset() আসল ঘড়ি ফিরিয়ে দেয় — না দিলে পরের টেস্টগুলোও নকল ঘড়িতে চলত। তৃতীয় টেস্টে assert.rejects নিজেই একটি Promise, তাই check সেটি await করে। নিজে ভেঙে দেখুন: debounce-এর 300 বদলে 200 করুন — প্রথম টেস্ট ✗ দেখাবে। আর removeAllListeners("warning") লাইনটি শুধু এই ডেমোর জন্য: Node 20-এ mock.timers experimental, তাই Node একটি সতর্কবার্তা ছাপে।

Async কোড টেস্টের একটি নিয়ম মনে রাখুন: প্রতিটি Promise-কে await করুন, বা ফেরত দিন। expect(p).rejects.toThrow()-এর আগে await ভুলে গেলে টেস্ট function শেষ হয়ে যায় Promise মীমাংসার আগেই। Jest-এর মতো runner তখন পাস দেখাতে পারে, অথচ আসল যাচাইটি ঘটেইনি; Vitest-এর সাম্প্রতিক version এমন না-await করা assertion নিয়ে সতর্ক করে। AI-এর লেখা টেস্টে এই ভুলটি প্রায়ই দেখা যায়।

৬ · Vitest, Testing Library, snapshot আর coverage

আজকের নতুন JavaScript প্রজেক্টে সবচেয়ে প্রচলিত test runner-গুলোর একটি Vitest. এটি তৈরি Vite-এর (পর্ব ৪৬) ওপর: আপনার অ্যাপ যে config আর রূপান্তর (TypeScript, JSX, ESM) দিয়ে চলে, টেস্টও সেভাবেই চলে — আলাদা Babel সেটআপ লাগে না। আর API প্রায় হুবহু Jest-এর মতো, তাই পুরোনো Jest টেস্ট সামান্য বদলেই চলে।

npm install -D vitest
npx vitest                # watch mode: ফাইল সেভ করলেই সংশ্লিষ্ট টেস্ট আবার চলে
npx vitest run            # একবার চালিয়ে থামে — CI-তে এটিই
npx vitest run --coverage # কোন লাইন টেস্টে চলেছে তার হিসাব

Watch mode দৈনন্দিন কাজের ধরন বদলে দেয়: এক পাশে কোড, অন্য পাশে টার্মিনাল; সেভ চাপামাত্র শুধু প্রভাবিত টেস্টগুলো আধা সেকেন্ডে চলে। আগের অংশের নকলগুলো Vitest-এ vi object-এ থাকে:

import { vi, it, expect, beforeEach, afterEach } from "vitest";
import { debounce } from "./debounce.js";
import { loadProducts } from "./products.js";

beforeEach(() => vi.useFakeTimers());                 // নকল ঘড়ি
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); });

it("debounce: থামার ৩০০ ms পরে একবার", () => {
  const search = vi.fn();
  const onType = debounce(search, 300);
  onType("শা"); onType("শাড়ি");
  vi.advanceTimersByTime(299);
  expect(search).not.toHaveBeenCalled();
  vi.advanceTimersByTime(1);
  expect(search).toHaveBeenCalledOnce();
  expect(search).toHaveBeenCalledWith("শাড়ি");
});

it("global fetch বদলে পণ্য লোড", async () => {
  const fakeFetch = vi.fn().mockResolvedValue(new Response('[{"name":"শাড়ি"}]'));
  vi.stubGlobal("fetch", fakeFetch);
  await expect(loadProducts()).resolves.toEqual(["শাড়ি"]);
});

// পুরো একটি module নকল করা (পর্ব ৪৫-এর import):
// vi.mock("./api.js", () => ({ getProducts: vi.fn(async () => []) }));

vi.stubGlobal কাজে লাগে যখন কোড dependency injection ছাড়াই global fetch ডাকে। vi.mock পুরো একটি module-এর import বদলে দেয় — কিন্তু যত বেশি module নকল, টেস্ট তত বেশি নিজের কল্পনা যাচাই করে। নিয়ম: সীমানার বাইরের জিনিস (নেটওয়ার্ক, ঘড়ি, payment gateway) নকল করুন; নিজের module যতটা পারেন আসল রাখুন।

DOM টেস্ট — jsdom আর Testing Library

Vitest ডিফল্টভাবে Node-এ চলে, যেখানে document নেই। DOM-এর কোড টেস্ট করতে একটি নকল ব্রাউজার পরিবেশ লাগে — jsdom বা তার দ্রুততর বিকল্প happy-dom. ফাইলের শুরুতে // @vitest-environment jsdom লিখুন, বা config-এ environment: "jsdom". এটি আসল ব্রাউজার নয় — layout নেই, CSS-এর হিসাব নেই — কিন্তু DOM, event আর form-এর জন্য যথেষ্ট আর অনেক দ্রুত।

এবার প্রশ্ন: পাতায় element খুঁজবেন কীভাবে? document.querySelector(".btn-primary-2") লিখলে টেস্ট একটি CSS class-এর ওপর নির্ভর করে, যা ডিজাইনার কাল বদলাবেন। Testing Library-এর দর্শন ভিন্ন: ব্যবহারকারী যেভাবে পাতা দেখেন, টেস্টও সেভাবে খুঁজবে। ব্যবহারকারী class দেখেন না; দেখেন “কার্টে যোগ করুন” লেখা একটি বোতাম। Screen reader-ও ঠিক এভাবেই পাতা পড়ে — role আর নাম দিয়ে:

// @vitest-environment jsdom
import { it, expect } from "vitest";
import { screen } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import { mountShop } from "./shop.js";

it("‘কার্টে যোগ করুন’ চাপলে কার্টের সংখ্যা বাড়ে", async () => {
  document.body.innerHTML = '<main id="app"></main>';
  mountShop(document.getElementById("app"));
  const user = userEvent.setup();

  await user.click(screen.getByRole("button", { name: "মধু কার্টে যোগ করুন" }));
  await user.type(screen.getByLabelText("পরিমাণ"), "2");

  expect(screen.getByRole("status").textContent).toBe("কার্টে ২টি পণ্য");
});

getByRole("button", { name }) শুধু তখনই বোতাম খুঁজে পায়, যখন সেটি সত্যিই একটি বোতাম (বা সঠিক ARIA role-সহ) আর তার নাম পড়া যায়। getByLabelText("পরিমাণ") কাজ করে শুধু যদি input-এর সাথে একটি সঠিক <label> যুক্ত থাকে। অর্থাৎ এমন টেস্ট লিখতে গিয়ে আপনার পাতা নিজে থেকেই আরও accessible হয়ে ওঠে — পর্ব ৩৯-এর form আর label-এর নিয়ম এখানে টেস্টের শর্ত। খোঁজার পছন্দের ক্রম: role → label → লেখা → শেষ উপায় data-testid.

Snapshot টেস্ট — সুবিধাজনক, কিন্তু ফাঁদ

Snapshot testing-এ আপনি প্রত্যাশিত মান হাতে লেখেন না। expect(renderReceipt(order)).toMatchSnapshot() প্রথমবার চললে ফলটি একটি .snap ফাইলে সেভ হয়; পরেরবার থেকে নতুন ফল সেই ফাইলের সাথে মেলানো হয়, আর পার্থক্য থাকলে diff দেখিয়ে ফেল। বড় output (একটি রসিদের HTML, একটি জটিল JSON) হঠাৎ বদলে গেলে ধরার জন্য এটি দ্রুত উপায়।

Snapshot কী ঠিক, তা জানে না — শুধু জানে কী আগে ছিল। প্রথম snapshot-এ বাগ থাকলে বাগটিই “সত্য” হিসেবে সেভ হয়। আর ৩০০ লাইনের diff দেখে মানুষ সাধারণত পড়ে না, vitest -u চেপে সব হালনাগাদ করে দেয় — টেস্ট সবুজ, কিন্তু কিছুই যাচাই হলো না। নিরাপদ ব্যবহার: ছোট snapshot (toMatchInlineSnapshot — টেস্ট ফাইলেই লেখা থাকে), শুধু স্থির output-এ, আর প্রতিটি snapshot বদল review-তে সত্যিই পড়া। গুরুত্বপূর্ণ নিয়ম — যেমন “মোট ৳৮৪০” — snapshot-এ নয়, একটি স্পষ্ট assertion-এ রাখুন।

Coverage — কতটুকু চলেছে, কতটুকু যাচাই হয়েছে নয়

Code coverage জানায় টেস্ট চলার সময় কোডের কত শতাংশ লাইন, শাখা (if-এর দুই দিক) আর function অন্তত একবার চলেছে। এটি খুব কাজের একটি মানচিত্র: লাল অংশ মানে সেখানে কোনো টেস্টই যায়নি — হয়তো ভুলের পথ, হয়তো ১০০% ছাড়ের শাখা।

কিন্তু ১০০% coverage মানে “১০০% সঠিক” নয়। Coverage শুধু দেখে লাইনটি চলেছে কি না, ফলটি যাচাই হয়েছে কি না নয়। it("withVat", () => { withVat(1000); }) — কোনো assertion নেই, তবু withVat-এর প্রতিটি লাইন “covered”. তাই coverage-কে লক্ষ্য না বানিয়ে প্রশ্নের উৎস বানান: “এই লাল শাখাটি কেন টেস্ট হয়নি?” আর টেস্ট সত্যিই কিছু যাচাই করে কি না মাপার আরও কঠিন উপায় — mutation testing — ভাবনার প্রশ্ন ৫-এ।

৭ · Playwright — আসল ব্রাউজারে end-to-end, আর flaky টেস্ট

Unit আর integration টেস্ট সব সবুজ, তবু চেকআউট বোতাম কাজ না-ও করতে পারে: হয়তো CSS একটি স্বচ্ছ স্তর দিয়ে বোতাম ঢেকে দিয়েছে, হয়তো Safari-তে একটি API নেই, হয়তো সার্ভারের route বদলে গেছে। এগুলো ধরে শুধু আসল ব্রাউজারে আসল অ্যাপ চালানো টেস্ট। Playwright (Microsoft, ২০২০) একই কোডে তিনটি ইঞ্জিনে টেস্ট চালায় — Chromium (Chrome/Edge), Firefox আর WebKit (Safari-র ইঞ্জিন)। সাধারণত চলে headless, অর্থাৎ জানালা না দেখিয়ে।

import { test, expect } from "@playwright/test";

test("কার্টে যোগ করে চেকআউটে মোট ৳৮৪০ দেখায়", async ({ page }) => {
  await page.goto("/");                                         // baseURL config থেকে

  await page.getByRole("button", { name: "চা-পাতা কার্টে যোগ করুন" }).click();
  await page.getByRole("button", { name: "চা-পাতা কার্টে যোগ করুন" }).click();
  await page.getByRole("button", { name: "মধু কার্টে যোগ করুন" }).click();
  await expect(page.getByRole("status")).toHaveText("কার্টে ৩টি পণ্য");

  await page.getByRole("link", { name: "চেকআউট" }).click();
  await expect(page).toHaveURL(/\/checkout$/);
  await expect(page.getByLabel("সর্বমোট")).toHaveText("৳৮৪০");   // ২ × ১২০ + ৬০০
});

কোথাও sleep বা “২ সেকেন্ড অপেক্ষা” নেই — এটিই Playwright-এর সবচেয়ে বড় উপহার, auto-waiting. click()-এর আগে Playwright নিজেই অপেক্ষা করে, যতক্ষণ না বোতামটি দেখা যায়, স্থির থাকে, সক্রিয় থাকে আর অন্য কিছুর নিচে চাপা না থাকে। await expect(locator).toHaveText("৳৮৪০") একবার দেখে হাল ছাড়ে না: মোট দাম fetch-এর পরে আসুক বা animation শেষে, লেখা না মেলা পর্যন্ত বারবার দেখে (ডিফল্ট ৫ সেকেন্ড)। page.getByRole(...) একটি locator — element খোঁজার একটি “রেসিপি”, যা প্রতিবার ব্যবহারের সময় নতুন করে খোঁজে; ভাষা Testing Library-র মতোই, role আর নাম।

npm init playwright@latest               # config, উদাহরণ আর ব্রাউজার ইনস্টল
npx playwright test                      # তিন ব্রাউজারে সব টেস্ট, একসাথে কয়েকটি worker-এ
npx playwright test --project=webkit     # শুধু Safari-র ইঞ্জিনে
npx playwright codegen localhost:5173    # ব্রাউজারে ক্লিক করুন, Playwright কোড লিখে দেয়
npx playwright show-report               # HTML রিপোর্ট; ফেল হলে trace খুলুন

দুটি টুল বিশেষভাবে কাজের। Codegen আপনার ক্লিক আর টাইপ রেকর্ড করে টেস্টের খসড়া লেখে — শুরুর জন্য ভালো, তবে বানানো locator আর assertion পরে নিজে গুছিয়ে নিন। Trace viewer একটি ফেল টেস্টের পুরো রেকর্ড দেখায়: প্রতিটি ধাপের আগে-পরে পাতার DOM snapshot, network request, console বার্তা — যেন সময়ে পিছিয়ে গিয়ে দেখা। Config-এ trace: "on-first-retry" দিলে CI-তে প্রথম retry-তে trace সেভ হয়, তাই CI-তে ফেল হওয়া টেস্টও নিজের মেশিনে খুঁটিয়ে দেখা যায়।

Flaky টেস্ট — কখনো পাস, কখনো ফেল

কোড একটুও না বদলে একই টেস্ট কখনো সবুজ, কখনো লাল — একে বলে flaky test. এটি শুধু বিরক্তিকর নয়, বিপজ্জনক: দল একবার “ওহ, ওটা মাঝে মাঝে এমনি লাল হয়” বলতে শিখলে আসল ভুলের লাল সংকেতও উপেক্ষা করবে। প্রায় সব flaky টেস্টের পেছনে তিনটি কারণ:

কারণলক্ষণসমাধান
সময়await page.waitForTimeout(1000); ধীর CI-তে ফেল; রাত ১২টার পরে তারিখের টেস্ট ভাঙেনির্দিষ্ট অপেক্ষার বদলে auto-wait আর retry করা assertion; fake timers; vi.setSystemTime দিয়ে স্থির তারিখ আর time zone
ভাগ করা অবস্থাএকা চালালে পাস, সবার সাথে চালালে ফেল; ক্রম বদলালে ফল বদলায়প্রতিটি টেস্টে beforeEach-এ নতুন অবস্থা; module-এর global variable নয়; প্রতিটি E2E টেস্টে আলাদা ব্যবহারকারী/ডেটা
নেটওয়ার্ক ও বাইরের জগৎবাইরের API ধীর বা বন্ধ; ডেটা বদলে গেছে; Math.randomনকল fetch; Playwright-এ page.route() দিয়ে নির্দিষ্ট request-এর বাঁধা উত্তর; random-এ seed বা injection

Retry (ফেল হলে আবার চালানো) শুধু লক্ষণ লুকায়, সমাধান নয়। কারণ খুঁজে ঠিক করুন, নইলে মুছে ফেলুন — অবিশ্বাসযোগ্য টেস্ট না থাকার চেয়েও খারাপ।

৮ · TDD, CI আর কী টেস্ট করবেন না

এ পর্যন্ত আগে কোড, পরে টেস্ট লিখেছি। Test-driven development (TDD) ক্রমটি উল্টে দেয়: আগে একটি ছোট, ব্যর্থ টেস্ট; তারপর সেটি পাস করানোর মতো ন্যূনতম কোড; তারপর পরিষ্কার করা। তিন রঙের একটি চক্র — red → green → refactor. একটি formatTaka function দিয়ে ধাপে ধাপে দেখুন, যা টাকার অঙ্ক বাংলায় দেখাবে:

  1. Redব্যর্থ টেস্টকী চাই, আগে লিখি
  2. Greenন্যূনতম কোডশুধু পাস করানো
  3. Refactorপরিষ্কার করাটেস্ট সবুজই থাকে
format.test.js🔴 প্রথম টেস্ট — function এখনো নেই
it("৮৪০ → ৳৮৪০", () => {
  expect(formatTaka(840)).toBe("৳৮৪০");
});
× ৮৪০ → ৳৮৪০
  TypeError: formatTaka is not a function
format.js🟢 ন্যূনতম কোড — “fake it”
export function formatTaka(n) {
  return "৳৮৪০";
}
✓ ৮৪০ → ৳৮৪০
// হাস্যকর? ইচ্ছা করেই। পরের টেস্ট একে সাধারণ হতে বাধ্য করবে।
format.test.js🔴 দ্বিতীয় টেস্ট — লাখের কমা
it("১২৫০০০ → ৳১,২৫,০০০ (লাখ-কোটির নিয়মে)", () => {
  expect(formatTaka(125000)).toBe("৳১,২৫,০০০");
});
× expected '৳৮৪০' to be '৳১,২৫,০০০'
format.js🟢 এবার আসল বাস্তবায়ন
export function formatTaka(n) {
  return "৳" + new Intl.NumberFormat("bn-BD").format(n);
}
✓ ৮৪০ → ৳৮৪০
✓ ১২৫০০০ → ৳১,২৫,০০০
format.js🔵 Refactor — আচরণ একই, গঠন ভালো
const bnNumber = new Intl.NumberFormat("bn-BD");   // একবার তৈরি

export const formatTaka = (n) => `৳${bnNumber.format(n)}`;
✓ ৮৪০ → ৳৮৪০
✓ ১২৫০০০ → ৳১,২৫,০০০
// পরের চক্র: ঋণাত্মক (ফেরত), পয়সা (৮৪০.৫০)…
  1. Red — আগে প্রত্যাশা। function লেখার আগেই টেস্ট: ৮৪০ দিলে "৳৮৪০" চাই। টেস্ট লাল হয়, কারণ function নেই। এই লাল জরুরি — এটি প্রমাণ করে টেস্টটি সত্যিই কিছু ধরতে পারে। যে টেস্ট কখনো লাল হয়নি, সে আদৌ কাজ করে কি না আপনি জানেন না।
  2. Green — ন্যূনতম কোড। সবচেয়ে সহজ যা পাস করায়: সরাসরি "৳৮৪০" ফেরত দেওয়া। শুনতে বোকামি, কিন্তু উদ্দেশ্য আছে — একসাথে একটি ছোট পা ফেলা, আর কোড যেন টেস্টের চেয়ে বেশি কিছু দাবি না করে।
  3. আবার Red — নতুন উদাহরণ। দ্বিতীয় টেস্ট নকল উত্তরটি ধরে ফেলল। একে বলে triangulation: দুটি আলাদা উদাহরণ কোডকে সাধারণ সমাধানে যেতে বাধ্য করে। আর দেখুন, টেস্টটি একটি স্থানীয় নিয়ম দলিল করছে — বাংলাদেশে কমা বসে লাখ-কোটির নিয়মে (১,২৫,০০০), পশ্চিমা ১২৫,০০০ নয়।
  4. Green — আসল বাস্তবায়ন। পর্ব ২০-এর Intl.NumberFormat("bn-BD") বাংলা অঙ্ক আর লাখের কমা দুটোই দেয়। দুটি টেস্টই সবুজ।
  5. Refactor — নিরাপদে পরিষ্কার। প্রতিটি কলে নতুন formatter না বানিয়ে একবার তৈরি করে রাখা হলো, function ছোট হলো। টেস্ট আবার চালালাম — এখনো সবুজ, তাই আচরণ বদলায়নি। তারপর পরের চক্র: ঋণাত্মক অঙ্ক বা পয়সা। প্রতিটি চক্র কয়েক মিনিটের।

স্পষ্ট নিয়মের হিসাব (টাকা, VAT, তারিখ) আর বাগ সারানোয় TDD দারুণ; নতুন UI-র নকশা খোঁজার সময় কম। তবে একটি অভ্যাস সবার জন্য: আগে বাগটি ঘটানো একটি ব্যর্থ টেস্ট, তারপর সমাধান। TDD-র বিতর্ক ভাবনার প্রশ্ন ২-এ।

CI — প্রতিটি push-এ সব টেস্ট

টেস্ট শুধু তখনই রক্ষা করে, যখন চলে। “আমি push-এর আগে চালাতে ভুলে গিয়েছিলাম” — এই সমস্যার সমাধান continuous integration (CI): প্রতিটি push বা pull request-এ একটি পরিষ্কার সার্ভারে নিজে থেকে সব টেস্ট চলে। লাল হলে merge আটকে যায়। GitHub Actions-এ এর জন্য রিপোজিটরিতে একটি ফাইলই যথেষ্ট:

name: test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci                                  # lockfile মেনে হুবহু একই প্যাকেজ
      - run: npm run lint                            # static analysis — পর্ব ৪৬
      - run: npm test                                # vitest run
      - run: npx playwright install --with-deps
      - run: npx playwright test                     # E2E

ক্রমটি লক্ষ করুন — সস্তা আগে, দামি পরে। Lint এক সেকেন্ডে ব্যর্থ হলে মিনিট খানেকের E2E চালানোর দরকারই নেই। npm ci lockfile হুবহু মানে, তাই “আমার মেশিনে চলে” আর CI-র মধ্যে প্যাকেজের version-এ পার্থক্য থাকে না।

কী টেস্ট করবেন না

  • Implementation detail: private helper, ভেতরের variable-এর নাম, কোন function কতবার ভেতরে ভেতরে ডাকা হলো, CSS class-এর নাম। এসব টেস্ট refactor-এর সময় অকারণে ভাঙে — অথচ refactor-এ সাহায্য করাই টেস্টের কাজ ছিল। যাচাই করুন আচরণ: public function কী ফেরত দেয়, পাতায় ব্যবহারকারী কী দেখেন।
  • অন্যের লাইব্রেরি: Intl.NumberFormat বাংলা অঙ্ক দেয় কি না, বা React-এর useState কাজ করে কি না — তাদের নিজস্ব টেস্ট আছে। আপনি যাচাই করুন আপনার ব্যবহার: formatTaka ঠিক ফল দেয় কি না।
পরীক্ষা ও ইন্টারভিউতে
  • Unit, integration আর E2E টেস্টের পার্থক্য? — unit একটি অংশ একা দেখে (দ্রুত), integration কয়েকটি অংশের জোড়া, E2E আসল ব্রাউজারে পুরো যাত্রা (ধীর, সবচেয়ে বাস্তব)।
  • Mock, stub আর spy-এর পার্থক্য? — stub বাঁধা উত্তর দেয়, spy কল রেকর্ড করে, mock প্রত্যাশিত কল যাচাই করে; Vitest-এর vi.fn() তিনটির কাজই করতে পারে।
  • toBe আর toEqual-এর পার্থক্য? — toBe Object.is দিয়ে একই মান বা reference দেখে; toEqual object-এর ভেতরের গঠন মেলায়।
  • setTimeout বা new Date()-নির্ভর কোড কীভাবে টেস্ট করবেন? — fake timers (vi.useFakeTimers, vi.setSystemTime) বা ঘড়িকে parameter হিসেবে inject করে।
  • TDD কী? — আগে ব্যর্থ টেস্ট (red), তারপর পাস করানোর ন্যূনতম কোড (green), তারপর পরিষ্কার (refactor) — ছোট ছোট চক্রে।

৯ · AI-এর যুগে — AI-এর লেখা টেস্ট কি আসলে কিছু যাচাই করে?

“এই function-এর টেস্ট লিখে দাও” — AI-কে দেওয়া সবচেয়ে প্রচলিত কাজগুলোর একটি, আর ফলও আসে চমৎকার দেখতে: বিশটি টেস্ট, সব সবুজ, coverage ৯৫%. বিপদটি ঠিক সেখানে। AI সাধারণত আপনার বাস্তবায়ন পড়ে টেস্ট লেখে, স্পেসিফিকেশন পড়ে নয়। ফলে কোডে যে বাগ আছে, টেস্টও সেটিকেই “সঠিক” ধরে নেয়।

AI-এর সাধারণ ভুলকেন বিপজ্জনকঠিক রূপ
প্রত্যাশিত মান কোডের সূত্র দিয়েই হিসাব: expect(withVat(1000)).toBe(1000 + 1000 * VAT_RATE)VAT_RATE ভুল হলে টেস্টও একই ভুল করে — চিরকাল সবুজহাতে হিসাব করা আক্ষরিক মান: toBe(1150)
শুধু mock-এর কল যাচাই: “fetch ডাকা হয়েছে”ফল ভুল হলেও পাস; টেস্ট নিজের নকলটিকেই পরীক্ষা করছেফল বা দৃশ্যমান আচরণ যাচাই; কল-যাচাই সহায়ক
সবকিছুর toMatchSnapshot()বর্তমান আচরণ (বাগসহ) “সত্য” হয়ে যায়গুরুত্বপূর্ণ নিয়ম স্পষ্ট assertion-এ
await ছাড়া expect(p).rejects, বা assertion ছাড়া টেস্টকিছু না দেখেই সবসময় পাসপ্রতিটি Promise await; প্রতিটি টেস্টে অন্তত একটি অর্থবহ assertion
শুধু “সুখী পথ” — সাধারণ inputedge case-এর বাগ থেকেই যায়খালি, শূন্য, ঋণাত্মক, সীমা, বাংলা input, ভুলের পথ

AI টেস্টে সবচেয়ে কাজে লাগে edge case খুঁজে দিতে — যে অবস্থাগুলো আপনার মাথায় আসেনি। তাই কোড না দেখিয়ে নিয়মগুলো দিন, আগে কেসের তালিকা চান, নিজে review করুন, তারপর টেস্ট:

AI-কে টেস্ট লেখাতে এমন prompt দিন — বাস্তবায়ন নয়, স্পেসিফিকেশন:

আমার একটি JavaScript function আছে: applyCoupon(subtotalPaisa, coupon). নিয়মগুলো: ১. দাম পয়সায় (পূর্ণসংখ্যা)। coupon = { type: "percent" | "flat", value }. ২. percent ০–১০০; flat টাকা পয়সায়। মোট কখনো ঋণাত্মক হবে না। ৩. ভুল input-এ (ঋণাত্মক subtotal, ১০০-র বেশি percent, অজানা type) RangeError. ৪. ফল Math.round দিয়ে নিকটতম পয়সায়। কাজ: ক. আগে শুধু টেস্ট কেসের একটি তালিকা দাও — input, প্রত্যাশিত ফল, আর কেন এই কেসটি দরকার। edge case আলাদা করে চিহ্নিত করো। আমি তালিকাটি review করব। খ. তারপর Vitest-এ it.each দিয়ে টেস্ট লেখো। প্রত্যাশিত মান হাতে হিসাব করা আক্ষরিক সংখ্যা হবে — কোডের সূত্র দিয়ে হিসাব নয়। গ. snapshot ব্যবহার করবে না। কোনো mock লাগবে না — এটি pure function. ঘ. শেষে বলো: কোন একটি ছোট বাগ (যেমন > এর বদলে >=) তোমার টেস্টগুলো ধরতে পারবে না?

AI-এর লেখা টেস্ট হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Java-র JUnit, Python-এর unittest আর JavaScript-এর Jest — সব test framework দেখতে এত একই রকম কেন? ১৯৯৪ সালের একটি Smalltalk লেখা থেকে কীভাবে পুরো “xUnit” পরিবার জন্মাল?

১৯৯৪ সালে Kent Beck Smalltalk ভাষার জন্য “Simple Smalltalk Testing: With Patterns” নামে একটি লেখা প্রকাশ করেন, সাথে ছোট একটি framework — SUnit. ধারণাগুলো আজ আমাদের কাছে স্বাভাবিক মনে হয়, কিন্তু তখন নতুন ছিল: প্রতিটি টেস্ট একটি আলাদা “test case”; প্রস্তুতির কোড একটি setUp-এ আর পরিষ্কারের কোড tearDown-এ (যাকে বলা হলো fixture); assertion ব্যর্থ হলে একটি exception; আর একটি runner, যা সব টেস্ট চালিয়ে ব্যর্থতা আর ভুল আলাদা করে রিপোর্ট করে। সবচেয়ে জরুরি: টেস্ট লেখা হয় একই ভাষায়, প্রোগ্রামার নিজে লেখেন — আলাদা কোনো QA দল বা টুল নয়।

১৯৯৭ সালে Beck আর Erich Gamma (বিখ্যাত “Design Patterns” বইয়ের লেখকদের একজন) জুরিখ থেকে আটলান্টায় OOPSLA সম্মেলনে যাওয়ার ফ্লাইটে বসে SUnit-এর নকশা Java-তে লেখেন — জন্ম নেয় JUnit. ১৯৯৮ সালে তাঁদের “Test Infected: Programmers Love Writing Tests” লেখাটি ধারণাটিকে ছড়িয়ে দেয়। এরপর প্রায় প্রতিটি ভাষায় একই নকশার অনুবাদ: CppUnit, NUnit (.NET), PyUnit — যা Python-এর standard library-তে unittest নামে ঢোকে। পুরো পরিবারকে একসাথে বলা হয় xUnit.

JavaScript একটু ভিন্ন পথ নেয়। ২০০৫ সালের দিকে Ruby-র RSpec “behaviour-driven development” (BDD)-এর ভাষা আনে: describe আর it, যাতে টেস্ট পড়তে বাক্যের মতো লাগে — “describe cart, it adds VAT”. JavaScript-এর Jasmine (২০১০) আর Mocha (২০১১) এই শৈলী গ্রহণ করে, Jest (২০১৪) আর Vitest সেটিই ধরে রাখে। কিন্তু ভেতরের কাঠামো একই xUnit: beforeEach হলো setUp, afterEach হলো tearDown, expect হলো assertion, আর runner একই রিপোর্ট দেয়। এই পর্বের দশ লাইনের test/expect আসলে SUnit-এর ধারণাই।

মূল উপলব্ধি: একটি ভালো ধারণা ভাষার সীমা মানে না। তাই একটি ভাষায় টেস্ট লেখা শিখলে অন্য যেকোনো ভাষার framework কয়েক ঘণ্টায় শেখা যায় — বদলায় শুধু নাম আর বানান, কাঠামো নয়।

প্র ০২TDD কি সত্যিই ভালো নকশা আনে? ২০১৪ সালে Ruby on Rails-এর স্রষ্টা “TDD is dead” ঘোষণা করলে যে বিতর্ক শুরু হয়, তা থেকে কী শেখা যায়?

Kent Beck ২০০২ সালে “Test-Driven Development: By Example” বইয়ে TDD-কে একটি পূর্ণ পদ্ধতি হিসেবে তুলে ধরেন। তিনি নিজেই বলেছেন, এটি “পুনরাবিষ্কার” — পুরোনো প্রোগ্রামাররা আগে প্রত্যাশিত output লিখে রাখতেন, তারপর প্রোগ্রাম। Extreme Programming আন্দোলনের সাথে TDD দ্রুত জনপ্রিয় হয়, আর কিছু দলে প্রায় ধর্মীয় নিয়মে পরিণত হয়: “টেস্ট ছাড়া একটি লাইনও নয়।”

২০১৪ সালের এপ্রিলে RailsConf-এ David Heinemeier Hansson (DHH), Ruby on Rails-এর স্রষ্টা, একটি বক্তৃতা আর “TDD is dead. Long live testing.” নামে একটি ব্লগ লেখায় এর বিরুদ্ধে কথা বলেন। তাঁর মূল অভিযোগ: শুধু unit টেস্ট সহজ করার জন্য অনেকে কোডকে অপ্রয়োজনীয় স্তর, interface আর mock-এ ভরে ফেলছেন — যাকে তিনি নাম দেন “test-induced design damage”. তাঁর মতে, ডেটাবেস আর framework-সহ বড় system টেস্টেই বেশি আত্মবিশ্বাস আসে। এরপর DHH, Kent Beck আর Martin Fowler ২০১৪ সালের মে–জুনে “Is TDD Dead?” নামে কয়েক পর্বের ভিডিও আলোচনা করেন — বিরল একটি সভ্য প্রকাশ্য বিতর্ক।

আলোচনায় দেখা যায় তিনজনের মধ্যে মিল আসলে বেশি। Beck নিজেও বলেন, তিনি সবকিছুতে TDD করেন না, আর অতিরিক্ত mock তাঁরও অপছন্দ। আসল প্রশ্ন দাঁড়ায় “TDD ভালো না খারাপ” নয়, বরং: কোন ধরনের কোডে আগে টেস্ট লিখলে লাভ? স্পষ্ট নিয়মের হিসাব (টাকা, তারিখ, parser), বাগ সারানো আর জটিল যুক্তিতে TDD নকশাকে সরল রাখে; অস্পষ্ট UI-র খোঁজাখুঁজি বা বাইরের সিস্টেমের সাথে জোড়ায় এর দাম বেশি। আর “টেস্ট সহজ করতে নকশা বদলানো” তখনই ভালো, যখন বদলটি কোডকে এমনিতেও পরিষ্কার করে — যেমন pure core আলাদা করা; খারাপ, যখন শুধু mock বসানোর জন্য স্তর বাড়ে।

মূল উপলব্ধি: পদ্ধতি একটি টুল, পরিচয় নয়। Red-green-refactor-এর আসল শিক্ষা — ছোট পা, দ্রুত প্রতিক্রিয়া, প্রতিটি টেস্টকে একবার লাল দেখা — TDD না মানলেও প্রতিদিনের কাজে নেওয়া যায়।

প্র ০৩Testing pyramid বলে “অনেক unit টেস্ট”, testing trophy বলে “বেশিরভাগ integration”. কে ঠিক? আর টুল বদলালে এই উত্তর কেন বদলায়?

Testing pyramid জনপ্রিয় হয় Mike Cohn-এর ২০০৯ সালের বই “Succeeding with Agile”-এর মাধ্যমে: নিচে অনেক unit টেস্ট, মাঝে service টেস্ট, ওপরে অল্প UI টেস্ট। প্রেক্ষাপটটি গুরুত্বপূর্ণ। তখনকার UI টেস্ট মানে Selenium দিয়ে আসল ব্রাউজার চালানো — প্রতিটি টেস্ট ধীর, প্রায়ই অকারণে ব্যর্থ, আর লেখা ও রক্ষণাবেক্ষণ ব্যয়বহুল। তাই যুক্তি ছিল অর্থনৈতিক: যে টেস্ট সস্তা আর নির্ভরযোগ্য, সেটি বেশি লিখুন। ২০১২ সালে Martin Fowler-এর একটি ছোট লেখা ছবিটিকে আরও ছড়িয়ে দেয়, আর উল্টো আকৃতিটি “ice-cream cone” নামে সতর্কবার্তা হয়ে যায়।

২০১৬ সালের শেষে Vercel-এর প্রতিষ্ঠাতা Guillermo Rauch একটি টুইট করেন: “Write tests. Not too many. Mostly integration.” ২০১৮ সালে Kent C. Dodds — Testing Library-র স্রষ্টা — এই ধারণা থেকে “testing trophy” আঁকেন: সবচেয়ে নিচে static analysis (ESLint, TypeScript), তারপর unit, সবচেয়ে মোটা অংশ integration, ওপরে E2E. তাঁর যুক্তি: ফ্রন্টএন্ডে একটি component-এর ভেতরের ছোট function প্রায়ই তুচ্ছ; বাগ হয় যখন component, state, event আর API একসাথে কাজ করে। আর jsdom ও Testing Library-র কারণে integration টেস্ট এখন প্রায় unit টেস্টের মতোই দ্রুত। একই সময়ে microservice জগতে Spotify-র প্রকৌশলীরা “testing honeycomb” প্রস্তাব করেন — সেখানেও মাঝের integration স্তর সবচেয়ে বড়।

লক্ষ করুন, দুটি ছবির মূল যুক্তি আসলে একই: প্রতি টাকা খরচে সবচেয়ে বেশি আত্মবিশ্বাস. বদলেছে খরচ। যখন integration আর E2E টেস্ট ধীর আর ভঙ্গুর ছিল, pyramid ঠিক ছিল। jsdom, Vitest-এর গতি আর Playwright-এর auto-waiting সেই খরচ কমিয়েছে, তাই আকৃতিও সরেছে। আবার একটি অ্যালগরিদম-ভারী লাইব্রেরি (ধরা যাক একটি তারিখের parser) আজও pyramid-ই চায়।

মূল উপলব্ধি: এই ছবিগুলো নিয়ম নয়, একটি অর্থনৈতিক হিসাবের সারাংশ। নিজের প্রজেক্টে জিজ্ঞেস করুন: কোথায় বাগ সবচেয়ে বেশি হয়, আর কোন স্তরের টেস্ট সেটি সবচেয়ে সস্তায় আর নির্ভরযোগ্যভাবে ধরে?

প্র ০৪Selenium থেকে Puppeteer হয়ে Playwright, আর Jest থেকে Vitest — JavaScript টেস্টিং টুলের প্রজন্ম কেন বারবার বদলায়? প্রতিটি নতুন টুল কোন পুরোনো ব্যথা সারিয়েছে?

ব্রাউজার স্বয়ংক্রিয় করার গল্প শুরু ২০০৪ সালে। ThoughtWorks-এ Jason Huggins একটি অভ্যন্তরীণ অ্যাপের পুনরাবৃত্ত পরীক্ষায় বিরক্ত হয়ে একটি JavaScript টুল লেখেন, যা পাতার ভেতরে বসে ক্লিক নকল করত — পরে যার নাম হয় Selenium. ব্রাউজারের নিরাপত্তা সীমার কারণে এই পদ্ধতি দুর্বল ছিল; তাই Simon Stewart-এর WebDriver ব্রাউজারকে বাইরে থেকে নিয়ন্ত্রণের পথ দেয়, আর দুটি প্রকল্প মিশে Selenium 2 হয়। ২০১৮ সালে WebDriver W3C মানদণ্ড হয়। Selenium-এর বড় দুর্বলতা ছিল সময়: টেস্ট লেখককে নিজে “অপেক্ষা করো যতক্ষণ না element আসে” লিখতে হতো, আর ভুল অপেক্ষাই ছিল flaky টেস্টের প্রধান উৎস।

২০১৭ সালে Chrome headless মোডে চলা শুরু করে, আর Google-এর Chrome DevTools দল প্রকাশ করে Puppeteer — DevTools protocol দিয়ে Chrome-কে সরাসরি, দ্রুত নিয়ন্ত্রণের একটি Node.js লাইব্রেরি। কিন্তু এটি মূলত শুধু Chrome-এর জন্য ছিল। ২০২০ সালের শুরুতে Microsoft প্রকাশ করে Playwright, যার মূল প্রকৌশলীদের কয়েকজন আগে Puppeteer-এ কাজ করতেন। নতুন দুটি সমাধান: একই API-তে Chromium, Firefox আর WebKit (Firefox আর WebKit-এর জন্য তাঁরা বিশেষভাবে প্রস্তুত build বানান), আর auto-waiting ও retry করা assertion ভেতরেই — অপেক্ষার দায় টেস্ট লেখকের কাঁধ থেকে টুলের কাঁধে।

Unit টেস্টের দিকেও একই ধারা। Facebook ২০১৪ সালে Jest open-source করে: প্রায় শূন্য config, jsdom ভেতরেই, পরে snapshot আর দারুণ watch mode — React-এর সাথে Jest দ্রুত প্রায় মানদণ্ড হয়ে যায়। কিন্তু JavaScript বদলাল: ES module, TypeScript, Vite-এর মতো নতুন build tool. Jest-কে প্রতিটি ফাইল Babel দিয়ে রূপান্তর করতে হতো, ESM সমর্থন বহুদিন অসম্পূর্ণ ছিল, আর অ্যাপের config আর টেস্টের config দুই জায়গায় রাখতে হতো। ২০২১ সালের শেষে Vite দলের Anthony Fu ও সহকর্মীরা আনেন Vitest: অ্যাপের Vite pipeline-ই টেস্টে ব্যবহার, ESM আর TypeScript স্বাভাবিকভাবে, অথচ API Jest-এর মতো, যাতে স্থানান্তর সহজ।

মূল উপলব্ধি: প্রতিটি প্রজন্ম আগেরটির সবচেয়ে বড় ব্যথা সারিয়েছে — Selenium হাতের কাজ, Playwright অপেক্ষার ভুল, Vitest config-এর দ্বৈততা। টুল বদলাবে; কিন্তু “কেন বদলাল” বুঝলে পরের টুলটি চিনতে আর বেছে নিতে সময় লাগে না।

প্র ০৫১০০% coverage-এর পরেও টেস্ট দুর্বল হতে পারে। Mutation testing কীভাবে “টেস্টের টেস্ট” নেয়, আর এর ধারণা কত পুরোনো?

Coverage প্রশ্ন করে: “টেস্ট চলার সময় এই লাইনটি কি চলেছে?” কিন্তু আসল প্রশ্ন হওয়া উচিত: “এই লাইনটি ভুল হলে কি কোনো টেস্ট টের পেত?” Mutation testing ঠিক দ্বিতীয় প্রশ্নটি করে। একটি টুল আপনার কোডে ইচ্ছা করে ছোট ছোট বাগ ঢোকায় — প্রতিটি বদলানো রূপকে বলে একটি mutant: >-কে >=, +-কে -, true-কে false, একটি function কল মুছে ফেলা, একটি string খালি করা। প্রতিটি mutant-এর ওপর পুরো টেস্ট চলে। কোনো টেস্ট লাল হলে mutant-টি “killed” — ভালো খবর। সব টেস্ট সবুজ থাকলে mutant “survived” — অর্থাৎ ওই বাগ আপনার কোডে ঢুকলে কেউ টের পেত না। মারা পড়া mutant-এর শতাংশই mutation score.

ধারণাটি পুরোনো। ১৯৭১ সালে Richard Lipton একটি ছাত্র-লেখায় প্রস্তাবটি দেন, আর ১৯৭৮ সালে DeMillo, Lipton আর Sayward-এর গবেষণাপত্র “Hints on Test Data Selection” একে প্রতিষ্ঠিত করে। পেছনের অনুমান দুটি: প্রোগ্রামাররা প্রায় সঠিক কোড লেখেন, ভুলগুলো সাধারণত ছোট (“competent programmer hypothesis”); আর যে টেস্ট ছোট ভুল ধরে, সে বড় ভুলও ধরে (“coupling effect”). কয়েক দশক এটি মূলত গবেষণাগারে ছিল, কারণ হাজার mutant-এর প্রতিটিতে পুরো টেস্ট চালানো তখনকার কম্পিউটারে অসম্ভব ব্যয়বহুল। দ্রুত মেশিন আর চালাক কৌশল — শুধু বদলানো লাইন ছোঁয়া টেস্ট চালানো, শুধু নতুন কোডে mutation — এখন একে ব্যবহারযোগ্য করেছে: Java-তে PIT, আর JavaScript/TypeScript-এ StrykerJS.

এই পর্বের AI-ভুলটি মনে করুন: expect(withVat(1000)).toBe(1000 + 1000 * VAT_RATE). Coverage ১০০%. কিন্তু Stryker যদি VAT_RATE-কে বদলায়, টেস্টও একই সাথে বদলে যায় — mutant বেঁচে থাকে, আর দুর্বলতাটি ধরা পড়ে। একই ভাবে if (qty > 0)-কে qty >= 0 করা mutant বাঁচলে বোঝা যায় শূন্য পরিমাণের কোনো টেস্ট নেই।

মূল উপলব্ধি: Coverage বলে টেস্ট কোথায় গেছে; mutation testing বলে টেস্ট কোথায় দেখেছে. প্রতিদিন না চালালেও, মাঝে মাঝে গুরুত্বপূর্ণ মডিউলে চালালে — বিশেষ করে AI-এর লেখা টেস্টে — অনেক “সবুজ কিন্তু অন্ধ” টেস্ট বেরিয়ে আসে।

অনুশীলন

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

  1. Edge case খুঁজুন: applyDiscount(amountPaisa, percent) একটি পরিমাণ থেকে শতাংশ ছাড় বাদ দেয়। কমপক্ষে ছয়টি টেস্ট কেসের নাম আর প্রত্যাশিত আচরণ লিখুন — শুধু সাধারণ উদাহরণ নয়।

    একটি ভালো তালিকা: (১) সাধারণ — ৮৪০০০, ১০ → ৭৫৬০০. (২) ০% → পরিমাণ অপরিবর্তিত। (৩) ১০০% → ০, ঋণাত্মক নয়। (৪) ১০০-র বেশি (১০১) → RangeError. (৫) ঋণাত্মক percent (−১০) → RangeError — নইলে “ছাড়” দাম বাড়িয়ে দেবে! (৬) পরিমাণ ০ → ০. (৭) গোল করা — ৯৯৯ পয়সায় ১৫% → ৮৪৯.১৫, তাই নিয়ম অনুযায়ী ৮৪৯. (৮) string "10" এলে? রূপান্তর করবেন, না ভুল দেবেন? অর্ধেক কেস আসলে পণ্যের সিদ্ধান্ত চায় — টেস্ট লেখা সেগুলো সামনে আনে।

  2. স্তর চিনুন: প্রতিটি কোন ধরনের টেস্ট — unit, integration না E2E? (ক) isBdPhone("01712345678") true দেয়। (খ) jsdom-এ form-এ ভুল নম্বর দিয়ে submit চাপলে ভুলের বার্তা দেখা যায়। (গ) Playwright-এ লগইন করে অর্ডার দিলে “অর্ডার নিশ্চিত” পাতা আসে। (ঘ) Express API-তে POST /orders পাঠালে একটি পরীক্ষামূলক ডেটাবেসে সারি তৈরি হয়। (ঙ) formatTaka(125000).

    (ক) unit — একটি pure function, একা। (খ) integration — form-এর HTML, event, validation আর বার্তা দেখানো একসাথে, তবে নকল ব্রাউজারে। (গ) E2E — আসল ব্রাউজার, আসল সার্ভার, পুরো যাত্রা। (ঘ) integration — API আর ডেটাবেসের জোড়া, ব্রাউজার ছাড়া (পর্ব ৫০-এ Express-এর সাথে এমন টেস্টের সুযোগ আসবে)। (ঙ) unit. (খ) অনেক দ্রুত, তাই validation-এর দশটি কেস সেখানে, আর E2E-তে শুধু একটি সুখী পথ।

  3. টেস্টের বাগ খুঁজুন: একজন সহকর্মী (বা AI) লিখেছেন — it("VAT", () => { expect(withVat(1000)).toBe(1000 * (1 + VAT_RATE)); }); আর it("load", () => { expect(loadProducts(fakeFetch)).resolves.toEqual(["শাড়ি"]); });. দুটি টেস্টই সবসময় সবুজ। কেন সমস্যা? ঠিক করুন।

    প্রথমটি প্রত্যাশিত মান কোডের সূত্র দিয়েই হিসাব করছে। কেউ ভুলে VAT_RATE = 1.5 লিখলে কোড আর টেস্ট দুটোই একসাথে ভুল — টেস্ট সবুজ। ঠিক রূপ: হাতে হিসাব করা আক্ষরিক মান, expect(withVat(1000)).toBe(1150). দ্বিতীয়টিতে await নেই: expect(...).resolves একটি Promise ফেরত দেয়, কিন্তু টেস্ট function তার জন্য অপেক্ষা না করেই শেষ হয়। ফল ভুল হলেও Jest-এর মতো runner আগেই পাস দেখিয়ে দিতে পারে (Vitest-এর সাম্প্রতিক version এমন ক্ষেত্রে সতর্ক করে)। ঠিক রূপ: it("load", async () => { await expect(loadProducts(fakeFetch)).resolves.toEqual(["শাড়ি"]); });. যাচাই করার সহজ উপায়: প্রত্যাশিত মান ইচ্ছা করে ভুল করুন — টেস্ট লাল না হলে টেস্টটিই ভাঙা।

  4. কোড লিখুন — ঘড়িকে টেস্টযোগ্য করা: function greeting() { const h = new Date().getHours(); return h < 12 ? "শুভ সকাল" : h < 18 ? "শুভ দুপুর" : "শুভ সন্ধ্যা"; } — এটি টেস্ট করা কঠিন, কারণ ফল নির্ভর করে কখন চালাচ্ছেন তার ওপর। Dependency injection দিয়ে বদলান, আর সীমানাসহ টেস্ট লিখুন।

    ঘড়িকে parameter বানান, ডিফল্ট আসল সময়:

    export function greeting(now = new Date()) {
      const h = now.getHours();
      return h < 12 ? "শুভ সকাল" : h < 18 ? "শুভ দুপুর" : "শুভ সন্ধ্যা";
    }
    
    it.each([
      [0, "শুভ সকাল"], [11, "শুভ সকাল"],     // সীমার দুই পাশ
      [12, "শুভ দুপুর"], [17, "শুভ দুপুর"],
      [18, "শুভ সন্ধ্যা"], [23, "শুভ সন্ধ্যা"],
    ])("%i টায় → %s", (hour, expected) => {
      expect(greeting(new Date(2026, 0, 1, hour))).toBe(expected);
    });

    অ্যাপের কোনো কল বদলায় না — greeting() আগের মতোই চলে। টেস্টে স্থির সময় দেওয়া যায়, আর সীমার দুই পাশ (১১ ও ১২, ১৭ ও ১৮) আলাদা করে দেখা হয়েছে — <-এর বদলে <= লেখার বাগ ঠিক সেখানেই ধরা পড়বে। বিকল্প: কোড না বদলে Vitest-এর vi.setSystemTime(). আর new Date(y, m, d, h) স্থানীয় সময় ধরে, তাই যেকোনো time zone-এ getHours() একই ঘণ্টা দেয়।

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

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

ক্রমএকটি JavaScript প্রজেক্টে বাগ রিপোর্ট আসার পর টেস্ট-নির্ভর পদ্ধতিতে কাজের ধাপগুলো সাজান (▲▼ চেপে বা টেনে)।

  1. 🐞 বাগটি নিজের মেশিনে ঘটিয়ে দেখা (reproduce)
  2. 🔴 বাগটি ঘটায় এমন একটি টেস্ট লেখা — সেটি ফেল করে
  3. 🟢 টেস্ট পাস করানোর মতো ন্যূনতম সমাধান
  4. 🧪 পুরো টেস্ট স্যুট চালানো — অন্য কিছু ভাঙেনি
  5. 🔵 কোড পরিষ্কার করা (refactor), টেস্ট সবুজই থাকে
  6. 🚀 push — CI-তে সব টেস্ট আবার চলে

Vitest-এ expect({ taka: 840 }).toBe({ taka: 840 }) চালালে কী হয়?

  • পাস করে, কারণ দুটির ভেতরের মান এক
  • ফেল করে, কারণ দুটি আলাদা object — reference এক নয়
  • SyntaxError দেয়
  • কখনো পাস, কখনো ফেল

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

  • Unit টেস্ট
  • End-to-end টেস্ট
  • Integration টেস্ট
  • হাতে করা manual টেস্ট

টেস্টে কোন test double একটি function কতবার আর কোন argument দিয়ে ডাকা হলো তা রেকর্ড করে, যাতে পরে যাচাই করা যায়?

  • Stub
  • Fake
  • Spy
  • Fixture

একটি JavaScript প্রজেক্টের টেস্টে ১০০% line coverage আছে। এর সঠিক অর্থ কী?

  • কোডে আর কোনো বাগ নেই
  • সব edge case টেস্ট হয়েছে
  • প্রতিটি ফলের সঠিকতা যাচাই হয়েছে
  • টেস্ট চলার সময় প্রতিটি লাইন অন্তত একবার চলেছে

Playwright-এ await expect(page.getByLabel("সর্বমোট")).toHaveText("৳৮৪০") লেখা হয়েছে, আর মোট দামটি একটি fetch শেষে প্রায় ১ সেকেন্ড পরে পাতায় আসে। কী ঘটে?

  • সাথে সাথে ফেল করে, কারণ তখনো লেখা আসেনি
  • লেখা মেলা পর্যন্ত বারবার দেখে, তারপর পাস করে
  • আগে waitForTimeout(1000) না দিলে সবসময় ফেল
  • পাতা নিজে থেকে reload করে আবার চেষ্টা করে

একটি JavaScript টেস্ট একা চালালে পাস করে, কিন্তু পুরো স্যুটের সাথে চালালে মাঝে মাঝে ফেল করে। সবচেয়ে সম্ভাব্য কারণ কী?

  • কম্পিউটারটি ধীর
  • Assertion লাইব্রেরিতে বাগ
  • টেস্টগুলোর মধ্যে ভাগ করা অবস্থা (shared state) — আগের টেস্টের রেখে যাওয়া পরিবর্তন
  • টেস্টটি describe-এর ভেতরে লেখা

শব্দকোষ

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

মনে রাখুন

  1. টেস্ট = জানা input, প্রত্যাশিত output, স্বয়ংক্রিয় তুলনা। এটি regression ধরে, নির্ভয়ে refactor করতে দেয়, চলমান দলিল হয়, আর সারানো বাগকে ফিরতে দেয় না।
  2. অনেক দ্রুত unit, কিছু integration, অল্প E2E — নিচে static analysis. একটি আচরণ সবচেয়ে নিচের যে স্তরে নির্ভরযোগ্যভাবে যাচাই হয়, সেখানেই।
  3. Arrange-Act-Assert; toBe reference, toEqual গঠন; ভুলে toThrow, async-এ await … rejects. টাকা পয়সায়, পূর্ণসংখ্যায়। Edge case-ই বাগের ঘর।
  4. বাইরের জগৎ নকল করুন, নিজের কোড নয়। Dependency injection দরজা খোলে; stub উত্তর দেয়, spy কল গোনে, fake timers সময় চালায়। প্রধান assertion ফলের ওপর।
  5. Vitest + Testing Library + Playwright + CI. Role দিয়ে খুঁজুন, sleep নয় auto-wait, snapshot আর coverage-এর সীমা জানুন, flaky টেস্ট সারান — আর AI-এর টেস্টে একটি বাগ ঢুকিয়ে দেখুন সেটি লাল হয় কি না।

পরের পর্বে: এবার ব্রাউজার ছেড়ে সার্ভারে। Node.js-এ ফাইল পড়া-লেখা, বড় ফাইলের জন্য stream, আর process দিয়ে command-line argument ও environment variable — এই পর্বের node:test সেখানে আপনার নিজের Node কোডের টেস্টে কাজে লাগবে।

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

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