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

Test pyramid আর integration test — contract test, end-to-end আর flaky test

The Test Pyramid and Integration Testing — Contract Tests, End-to-End and Flaky Tests

Taught in Bangla, this lesson explains the test pyramid as a budget, modelling suite time and false red builds, and compares the testing trophy, honeycomb and ice-cream cone. It covers big-bang, top-down, bottom-up and sandwich integration, testing against real databases with rollback, consumer-driven contract tests, end-to-end tests with page objects, flaky test detection and quarantine, and suite speed in CI.

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

  • Test pyramid কেন, তার পেছনের হিসাব — suite-এর সময় আর “মিথ্যা লাল build”-এর সম্ভাবনা, হাতে আর চালানো যায় এমন মডেলে
  • Trophy, honeycomb আর ice-cream cone — কোন পরিস্থিতিতে কোন আকার যুক্তিসংগত
  • Integration-এর চারটি কৌশল (big-bang, top-down, bottom-up, sandwich) আর narrow বনাম broad integration test
  • আসল database-এর সাথে test, transactional rollback আর test data; consumer-driven contract আর provider verification
  • End-to-end test আর page object; flaky test শনাক্ত, quarantine আর ঠিক করা; suite-এর গতি, parallelism আর test environment

১ · Test pyramid — কোন স্তরে কতগুলো test

পর্ব ৪৫-এ (Testing-এর মৌলিক) test-এর স্তরগুলো দেখেছেন: unit test, integration test, system test আর acceptance test. পর্ব ৪৬ থেকে ৫০-এ শিখেছেন ভালো unit test লেখা, test case নকশা, TDD, test double আর coverage. এই পর্বের প্রশ্ন আলাদা: একটি পুরো project-এ কোন স্তরে কতগুলো test রাখবেন, আর বড় test-গুলোকে কীভাবে বিশ্বাসযোগ্য রাখবেন।

একটি বিয়েবাড়ির রান্না ভাবুন। বাবুর্চি প্রতিটি হাঁড়ির লবণ চামচে চেখে দেখেন — সেকেন্ডের কাজ, দিনে শত বার (unit test). মাঝে মাঝে দেখেন পোলাও আর রোস্ট একসাথে প্লেটে মানাচ্ছে কি না (integration test). আর আগের দিন একবার পুরো আয়োজনের মহড়া — অতিথি আসা থেকে পরিবেশন পর্যন্ত (end-to-end test). মহড়া সবচেয়ে বাস্তব, কিন্তু দামি আর ধীর; প্রতিটি লবণের ভুল মহড়ায় খুঁজলে বিয়েই হবে না। আবার শুধু চাখলে জানবেন না প্যান্ডেলে খাবার পৌঁছাবে কি না। দরকার একটি মিশ্রণ, আর তার অনুপাত।

সেই অনুপাতের সবচেয়ে পরিচিত ছবি test pyramid. Mike Cohn তাঁর Succeeding with Agile (২০০৯) বইয়ে “test automation pyramid” নামে ছবিটি জনপ্রিয় করেন; তাঁর নিজের বক্তব্য অনুযায়ী ধারণাটি নিয়ে তিনি এর কয়েক বছর আগে থেকেই কথা বলছিলেন। Cohn-এর তিনটি স্তর ছিল নিচ থেকে Unit, Service আর UI. পরে Martin Fowler-এর “TestPyramid” লেখা (২০১২) আর Ham Vocke-এর “The Practical Test Pyramid” (২০১৮, Fowler-এর ওয়েবসাইটে) ছবিটিকে আরও ছড়িয়ে দেয়। আজ মাঝের স্তরটিকে অনেকে integration বলেন, ওপরেরটিকে end-to-end (E2E).

তিনটি আকার: pyramid, trophy আর ice-cream cone

Width of each band = roughly how many tests of that kind a team keeps

Test pyramid, testing trophy আর ice-cream cone-এর তুলনা E2E Service Unit E2E Integration Unit Static Manual E2E / UI Service Unit Pyramid Trophy Ice-cream cone Cohn, ২০০৯ Dodds, ~২০১৮ anti-pattern
ছবি ৫১.১ — প্রতিটি পট্টির প্রস্থ মোটামুটি সেই ধরনের test-এর সংখ্যা বোঝায়, সঠিক অনুপাত নয়। Pyramid-এ ওপরে উঠলে test কমে; trophy-তে সবচেয়ে মোটা অংশ integration, আর নিচে static check (type checker, linter); ice-cream cone-এ ছবি উল্টে গেছে — সবচেয়ে বেশি হাতে করা (Manual) আর UI test, সবচেয়ে কম unit.

কেন নিচে বেশি — চারটি কারণ

বৈশিষ্ট্যUnitIntegration / serviceE2E / UI
গতি (একটি test)মিলিসেকেন্ডদশমিক সেকেন্ড থেকে কয়েক সেকেন্ডকয়েক সেকেন্ড থেকে মিনিট
লেখা ও রক্ষণাবেক্ষণের খরচকমমাঝারি: database, container, test dataবেশি: পুরো system, browser, data সাজানো
স্থিরতাপ্রায় কখনো flaky নয়মাঝে মাঝে (network, ক্রম, পুরোনো data)সবচেয়ে বেশি flaky (অপেক্ষা, animation, বাইরের service)
Feedback কত নির্দিষ্ট“এই function-এর এই শর্ত”“এই adapter আর database-এর জোড়া”“checkout ভাঙছে” — কোথায়, খুঁজতে হবে
কী প্রমাণ করেযুক্তি ঠিকঅংশগুলো আসলেই একসাথে কথা বলেব্যবহারকারী কাজটি শেষ করতে পারেন

শেষ সারিটি খেয়াল করুন — ওপরের test-এর একটি অনন্য মূল্য আছে। তাই pyramid বলে না “E2E খারাপ”; বলে প্রতিটি ঝুঁকি সবচেয়ে নিচের যে স্তরে ধরা যায়, সেখানেই ধরুন. Discount-এর হিসাব ২০টি boundary-তে — ২০টি unit test; “discount-সহ অর্ডার সত্যিই database-এ জমা হয়” — একটি integration test; “ব্যবহারকারী coupon দিয়ে টাকা দিতে পারেন” — একটি E2E test.

“Unit” কতটুকু, তা নিয়ে দলগুলো একমত নয় (পর্ব ৪৬): কেউ একটি class-কে একা চালান (Fowler-এর ভাষায় solitary), কেউ কয়েকটি সহযোগী class একসাথে (sociable). Software Engineering at Google (২০২০) বইয়ে বর্ণিত ভাগ আবার স্তর নয়, আকার — small, medium, large — test কোন সম্পদ (একটি process, একটি যন্ত্র, network) ছুঁতে পারে তা দিয়ে। নামের চেয়ে প্রশ্নটি গুরুত্বপূর্ণ: test কত দ্রুত, কত স্থির, আর ভাঙলে কারণ কত তাড়াতাড়ি পাওয়া যায়।

২ · Pyramid-এর হিসাব — suite-এর সময় আর মিথ্যা লাল build

ছবির কথা হিসাবে নামানো যাক। দুটি সংখ্যা দলের প্রতিদিনের জীবন ঠিক করে: পুরো suite চলতে কত সময় লাগে, আর কোডে ভুল না থাকলেও build লাল হওয়ার সম্ভাবনা কত — একে বলে false failure বা মিথ্যা লাল।

সময়: T = Σ (test-এর সংখ্যা × একটি test-এর সময়)। কয়েকটি যন্ত্রে ভাগ করে চালালে আদর্শ অবস্থায় T ÷ যন্ত্রের সংখ্যা (অংশ ৯)।

মিথ্যা লাল: ধরুন একটি সুস্থ test একবার চালালে ভুল করে fail করার সম্ভাবনা p. তাহলে সেটি pass করার সম্ভাবনা (1 − p). n-টি এমন test স্বাধীনভাবে চললে সবগুলো pass করার সম্ভাবনা (1 − p)n, আর অন্তত একটি fail করে build লাল হওয়ার সম্ভাবনা

P(মিথ্যা লাল) = 1 − Π (1 − pi)ni

যেখানে i প্রতিটি স্তর। ছোট p-তে একটি কাজের অনুমান: 1 − (1 − p)n ≈ n × p, যতক্ষণ n × p ছোট (ধরুন ০.১-এর নিচে)। অর্থাৎ flaky হওয়ার সম্ভাবনা যোগ হয়: ৫০০-তে একবার flaky এমন ২০টি test মিলে প্রায় ৪% build মিথ্যা লাল করে।

নিচের মডেলে দুটি কাল্পনিক suite. প্রতিটি স্তরের সময় আর flaky-হার অনুমান — মাপা data নয়, শুধু আকারের পার্থক্য দেখানোর জন্য বেছে নেওয়া:

// Test-suite cost model. ASSUMPTIONS (invented for the model, not measured data):
// seconds per test and chance that ONE run of ONE healthy test fails anyway.
const LEVEL = {
  unit:        { sec: 0.005, flake: 0.00001 },   // 5 ms, 1 in 100,000
  integration: { sec: 0.5,   flake: 0.0002 },    // 0.5 s, 1 in 5,000
  e2e:         { sec: 20,    flake: 0.002 },     // 20 s, 1 in 500
};
const SUITES = {
  "pyramid":         { unit: 2000, integration: 200, e2e: 20 },
  "ice-cream cone":  { unit: 100,  integration: 50,  e2e: 300 },
};
const WORKERS = 8;                      // parallel machines in CI

function analyse(counts) {
  let sec = 0, pAllPass = 1;
  for (const [lvl, n] of Object.entries(counts)) {
    sec += n * LEVEL[lvl].sec;
    pAllPass *= Math.pow(1 - LEVEL[lvl].flake, n);  // independent flakes
  }
  return { sec, falseRed: 1 - pAllPass };
}

for (const [name, counts] of Object.entries(SUITES)) {
  const r = analyse(counts);
  const total = Object.values(counts).reduce((a, b) => a + b, 0);
  console.log(`${name}: ${total} tests`);
  console.log(`  serial time   : ${(r.sec / 60).toFixed(1)} min`);
  console.log(`  ${WORKERS} workers     : ${(r.sec / WORKERS / 60).toFixed(1)} min (ideal split)`);
  console.log(`  false red build: ${(100 * r.falseRed).toFixed(1)}% of runs`);
  console.log(`  20 runs/day -> ${(20 * r.falseRed).toFixed(1)} false alarms/day`);
}

আউটপুট:

pyramid: 2220 tests
  serial time   : 8.5 min
  8 workers     : 1.1 min (ideal split)
  false red build: 9.5% of runs
  20 runs/day -> 1.9 false alarms/day
ice-cream cone: 450 tests
  serial time   : 100.4 min
  8 workers     : 12.6 min (ideal split)
  false red build: 45.8% of runs
  20 runs/day -> 9.2 false alarms/day

হাতে মিলিয়ে নিন। Pyramid-এর সময়: ২০০০ × ০.০০৫ = ১০ সেকেন্ড, ২০০ × ০.৫ = ১০০, ২০ × ২০ = ৪০০; মোট ৫১০ সেকেন্ড = ৮.৫ মিনিট। মিথ্যা লালের অনুমান: ০.০২ + ০.০৪ + ০.০৪ = ০.১০; ঠিক হিসাব ৯.৫%, কারণ n × p পুরো ছোট নয়। Ice-cream cone-এ test পাঁচ ভাগের এক ভাগ, অথচ সময় প্রায় ১২ গুণ (শুধু E2E-তে ৩০০ × ২০ = ৬০০০ সেকেন্ড), আর প্রায় অর্ধেক build কোনো ভুল ছাড়াই লাল — দিনে ৯টি মিথ্যা সতর্কবার্তা, আর কিছুদিন পরে কেউ লাল দেখে থামবেন না। Pyramid-এও সময়ের প্রায় ৮০% মাত্র ২০টি E2E-এর, আর মিথ্যা লালের প্রায় ৪০% (০.০৪ ÷ ০.১০)। মডেলের সীমা: flake-গুলো স্বাধীন ধরা (বাস্তবে একটি ধীর database একসাথে অনেক test ফেলে), আর যন্ত্রে নিখুঁত ভাগ ধরা।

মূল ধারণা

Test suite-এর মূল্য তার সংকেতের বিশ্বাসযোগ্যতায়। লাল মানে “ভুল আছে” আর সবুজ মানে “নিশ্চিন্তে merge করুন” — এটি যত সত্য, suite তত কাজের। ধীর আর flaky test শুধু সময় নেয় না, সংকেতটিকেই দুর্বল করে। তাই প্রতিটি ধীর, ভঙ্গুর test-কে নিজের জায়গা “অর্জন” করতে হয়।

৩ · অন্য আকার — trophy, honeycomb আর ice-cream cone

Pyramid সার্বজনীন সত্য নয়; এটি একটি নির্দিষ্ট যুগের, নির্দিষ্ট ধরনের system-এর পরামর্শ। পরের বছরগুলোতে কয়েকটি বিকল্প আকার এসেছে, প্রত্যেকটি একটি নির্দিষ্ট অভিযোগের উত্তরে।

Testing trophy

Testing trophy: Kent C. Dodds-এর প্রস্তাব (আনুমানিক ২০১৮), মূলত JavaScript frontend-এর জন্য। নিচে static check, তারপর unit, সবচেয়ে বড় integration, ওপরে অল্প E2E. যুক্তি: UI কোডের খুঁটিনাটির unit test implementation-এ বাঁধা পড়ে refactor-এ ভাঙে, অথচ কয়েকটি component একসাথে চালানো test আস্থা বেশি দেয়। Guillermo Rauch-এর একটি টুইট (২০১৬) — “Write tests. Not too many. Mostly integration.” — এই ধারার স্লোগান।

Test honeycomb

Honeycomb: Spotify-এর engineering blog-এর “Testing of Microservices” (André Schaffer, ২০১৮) লেখায় বর্ণিত। ছোট microservice-এ ভেতরের যুক্তি কম, ঝুঁকি সীমানায় — database আর অন্য service-এর সাথে কথায়। তাই প্রধান ভাগ service-কে তার সীমানায় যাচাই করা integration test; ভেতরের খুঁটিনাটির test আর বহু service জোড়া test — দুটিই অল্প।

Ice-cream cone

Ice-cream cone: আকার নয়, সতর্কবার্তা। নামটি আনুমানিক ২০১২ সালে Alister Scott-এর একটি blog-এর সাথে যুক্ত। ওপরে হাতে করা test-এর বড় স্কুপ, তার নিচে অনেক UI test, অল্প unit. সাধারণত তৈরি হয় যখন test-এর দায় শুধু আলাদা QA দলের, আর কোড test করার মতো করে (seam রেখে) লেখা হয়নি।

তাহলে কী নেবেন? তিনটি আকারই একই প্রশ্নের ভিন্ন উত্তর: আপনার system-এ ঝুঁকি কোথায়? হিসাবের নিয়মে ভরা library-তে ঝুঁকি যুক্তিতে — pyramid; component-এ ভরা frontend-এ জোড়ায় — trophy; database আর API-তে কথা বলা ছোট service-এ সীমানায় — honeycomb. আর সব আকারেই দুটি কথা একই: ধীর, ভঙ্গুর E2E অল্প, আর ice-cream cone নয়। নামে আটকাবেন না — trophy-র “integration” প্রায়ই browser বা network ছাড়া, তাই দ্রুত; গতি আর স্থিরতা দেখুন।

৪ · Integration-এর কৌশল — big-bang, top-down, bottom-up আর sandwich

পাঠ্যবইয়ের (Pressman, Sommerville) পুরোনো প্রশ্ন: আলাদা আলাদা তৈরি module কোন ক্রমে জোড়া দিয়ে test করবেন? চারটি প্রচলিত উত্তর:

  • Big-bang: সব module তৈরি হলে একবারে সব জোড়া, তারপর test. সহজ পরিকল্পনা, কিন্তু ব্যর্থ হলে কোন জোড়ায় ভুল — খুঁজে পাওয়া কঠিন, আর ভুল ধরা পড়ে একেবারে শেষে।
  • Top-down: ওপরের নিয়ন্ত্রক module থেকে শুরু; নিচের যেগুলো এখনো তৈরি হয়নি, তাদের জায়গায় stub — ঠিক করা উত্তর দেওয়া নকল। একটি একটি করে stub-কে আসল module দিয়ে বদলান। সুবিধা: মূল প্রবাহ আর UI তাড়াতাড়ি দেখা যায়। অসুবিধা: অনেক stub লিখতে হয়, আর নিচের আসল আচরণ দেরিতে আসে।
  • Bottom-up: সবচেয়ে নিচের module (database-এর adapter, payment client) থেকে শুরু; ওপরের ডাকার module-এর জায়গায় test driver — ছোট কোড, যা module-টিকে ডাকে আর ফল যাচাই করে। সুবিধা: ভিত্তি আগে পোক্ত। অসুবিধা: পুরো ব্যবহারকারীর প্রবাহ দেখা যায় শেষে।
  • Sandwich (hybrid): ওপর থেকে stub দিয়ে আর নিচ থেকে driver দিয়ে একসাথে, মাঝের স্তরে মিলন। বড় project-এ দুটি দল সমান্তরালে কাজ করতে পারে।

নিচের stepper-এ একটি কাল্পনিক অনলাইন বাজার অ্যাপের চারটি module: OrderController (A, ওপরে), যে ডাকে OrderService (B), আর B ডাকে PaymentClient (C) আর InventoryRepo (D). ভেতরে একটি ভুল লুকানো: B টাকার অঙ্ক পাঠায় টাকায় (450), কিন্তু C ধরে নেয় অঙ্কটি পয়সায়।

A → B → (C, D) · লুকানো ভুল: B–C-তে টাকা বনাম পয়সা
কৌশল
আসল module
নকল
Test-এর ফল
সন্দেহের তালিকা
  1. Big-bang. চারটি module একসাথে জুড়ে checkout: ৪৫০ টাকার অর্ডারে কাটা হলো ৪.৫০ টাকা। ভুল A–B, B–C না B–D-তে — test বলছে না; তিনটি জোড়াই সন্দেহের তালিকায়।
  2. Top-down, ধাপ ১. শুধু A আর B আসল; C আর D-এর stub ঠিক করা উত্তর দেয় (“payment ok”, “stock 5”). Pass — A–B জোড়া ঠিক।
  3. Top-down, ধাপ ২. D-এর stub সরিয়ে আসল InventoryRepo, test database-সহ। Pass — B–D-ও ঠিক।
  4. Top-down, ধাপ ৩. শেষ stub সরিয়ে আসল PaymentClient: fail. এই ধাপে নতুন শুধু B–C, তাই ভুল সেখানেই — C-এর নথিতে একক “পয়সা”, অথচ B পাঠায় charge(450).
  5. Bottom-up. উল্টো দিক থেকে: C আর D আগে driver দিয়ে — C-এর driver নথি মেনে charge(45000) পাঠাল, pass. তারপর B যোগ করতেই fail — সন্দেহ আবার শুধু নতুন জোড়ায়।
  6. Sandwich. ওপরের দল A+B-কে stub দিয়ে, নিচের দল C আর D-কে driver দিয়ে সমান্তরালে যাচাই করল; মাঝের B-তে মিলতেই fail. তিন ধাপে-ধাপে কৌশলের মূল সুবিধা একই: একবারে একটি নতুন জোড়া, তাই ব্যর্থতার জায়গা নিজে থেকেই চিহ্নিত।

আজকের দলে এই নামগুলো কম শোনা যায়, কারণ continuous integration (পর্ব ৫৮) দিনে কয়েকবার ছোট ছোট পরিবর্তন জোড়ে — কার্যত চিরস্থায়ী incremental integration. শিক্ষাটি একই: একসাথে যত কম নতুন জোড়া, ভুল খোঁজা তত সহজ। Stub আর driver আজ test double আর test harness নামে রোজকার হাতিয়ার (পর্ব ৪৯ Test double).

Narrow বনাম broad integration test

“Integration test” কথাটি এত রকম অর্থে ব্যবহার হয় যে Martin Fowler তাঁর “IntegrationTest” লেখায় (২০১৮) দুটি ভাগ প্রস্তাব করেন:

Narrow

Narrow integration test: নিজের কোডের শুধু সেই অংশ, যা বাইরের একটি জিনিসের সাথে কথা বলে — যেমন একটি repository আর আসল database, বা HTTP client আর একটি নকল server. বাকি system নেই। দ্রুত, নির্দিষ্ট, অনেকগুলো রাখা যায়। পর্ব ৪০-এর (Clean আর hexagonal architecture) driven adapter-ই এর স্বাভাবিক লক্ষ্য — adapter হলো integration-এর seam.

Broad

Broad integration test: অনেক service-এর চালু রূপ একসাথে — order, payment, inventory সব চালিয়ে একটি পথ। বেশি আস্থা, কিন্তু ধীর, সাজানো কঠিন, আর ভাঙলে কারণ খোঁজা কঠিন। অনেক দল এগুলোর বড় অংশ contract test (অংশ ৬) দিয়ে বদলায়।

৫ · আসল dependency-র সাথে test — database, rollback আর test data

পর্ব ৪৯-এ দেখেছেন fake আর mock দিয়ে dependency আলাদা করা। কিন্তু একটি repository-র SQL ঠিক আছে কি না, তা fake দিয়ে প্রমাণ হয় না — fake-এর ভেতরে তো SQL-ই নেই। এখানে দরকার আসল database. বিকল্প দুটি:

বিকল্পসুবিধাঝুঁকি
In-memory database (যেমন SQLite :memory:, Java-জগতে H2)খুব দ্রুত; কিছু install করতে হয় না; প্রতিটি test নতুন database পায়Production-এ PostgreSQL বা MySQL হলে SQL-এর রূপ, type, constraint আর transaction-এর আচরণে পার্থক্য — test সবুজ, production-এ ভুল
Container-এ আসল database (যেমন Testcontainers)Production-এর একই engine আর সংস্করণ; migration-ও একইপ্রথম চালু হতে কয়েক সেকেন্ড; CI-তে Docker লাগে; একটু বেশি সাজানো

Testcontainers একটি open-source library (Java দিয়ে শুরু, আনুমানিক ২০১৫; এখন নানা ভাষায়): test-এর আগে Docker-এ database container চালু করে, শেষে মুছে ফেলে। Python-এ মোটামুটি এমন (লেখার সময়ের API; documentation মিলিয়ে নিন):

import pytest, sqlalchemy
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def engine():
    with PostgresContainer("postgres:16") as pg:      # same engine as production
        eng = sqlalchemy.create_engine(pg.get_connection_url())
        run_migrations(eng)                           # the real schema, not a copy
        yield eng                                     # container removed after the session

@pytest.fixture
def conn(engine):
    with engine.connect() as c:
        tx = c.begin()
        yield c                                       # the test runs inside this transaction
        tx.rollback()                                 # every write disappears

লক্ষ করুন দুটি কৌশল একসাথে: container চালু হয় একবার পুরো session-এ (ধীর কাজটি ভাগ করা), আর প্রতিটি test চলে নিজের transaction-এ, শেষে ROLLBACK — একে বলে transactional rollback. পরের test পায় পরিষ্কার database, কিন্তু নতুন container-এর খরচ ছাড়া। (Python-এর pytest fixture-এর খুঁটিনাটি Python · pytest দিয়ে testing-এ।)

নিজে চালিয়ে দেখুন — rollback দিয়ে পরিষ্কার অবস্থা

নিচের live SQL এডিটরে একটি কাল্পনিক অর্ডার repository-র দুটি test. প্রতিটি test একটি BEGIN দিয়ে শুরু, assertion হিসেবে একটি SELECT … CASE, আর শেষে ROLLBACK. Test ২ ধরে নেয় test শুরুর সময় customer ১-এর কোনো অর্ডার নেই:

-- test 1: saving an order stores its total
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (101, 1, 450);
SELECT 'test 1' AS test,
  CASE WHEN (SELECT total FROM orders WHERE id = 101) = 450
       THEN 'PASS' ELSE 'FAIL' END AS result;
ROLLBACK;

-- test 2: a customer with one new order has exactly one order
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (201, 1, 900);
SELECT 'test 2' AS test,
  CASE WHEN (SELECT COUNT(*) FROM orders WHERE customer_id = 1) = 1
       THEN 'PASS' ELSE 'FAIL' END AS result;
ROLLBACK;

SELECT COUNT(*) AS orders_left_after_suite FROM orders;

রানার প্রতিটি স্টেটমেন্টের ফল আলাদা দেখায়: ✓ BEGIN, INSERT-এ একটি সারি, তারপর test 1 | PASS, ✓ ROLLBACK; একইভাবে test 2 | PASS; আর শেষে orders_left_after_suite = 0. দুটি test-ই নিজের data নিজে বানাল, আর ROLLBACK সেটি মুছে দিল — suite শেষে database ঠিক শুরুর মতো। Test ২ জানেই না test ১ চলেছিল কি না; যেকোনো ক্রমে চালালে একই ফল। এই গুণের নাম test isolation. BEGIN, COMMIT আর ROLLBACK-এর বিস্তারিত DBMS · Transaction আর ACID-এ।

এবার একই suite, শুধু test ১-এর শেষে কেউ ভুল করে COMMIT লিখেছেন (বা test framework-এর কোনো ধাপ নিজেই commit করে ফেলে):

-- test 1, with a COMMIT by mistake
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (101, 1, 450);
SELECT 'test 1' AS test,
  CASE WHEN (SELECT total FROM orders WHERE id = 101) = 450
       THEN 'PASS' ELSE 'FAIL' END AS result;
COMMIT;

-- test 2, unchanged
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (201, 1, 900);
SELECT 'test 2' AS test,
  CASE WHEN (SELECT COUNT(*) FROM orders WHERE customer_id = 1) = 1
       THEN 'PASS' ELSE 'FAIL' END AS result;
ROLLBACK;

SELECT COUNT(*) AS orders_left_after_suite FROM orders;

এবার test 1 | PASS, কিন্তু test 2 | FAIL, আর শেষে orders_left_after_suite = 1. Test ২-এর নিজের কোডে কোনো ভুল নেই — একা চালালে pass করে। সে fail করছে কারণ test ১ একটি অর্ডার রেখে গেছে। এমন test-কে বলে order-dependent test: ফল নির্ভর করে আগে কী চলেছে তার ওপর। Test runner যদি ক্রম এলোমেলো করে বা কয়েকটি process-এ ভাগ করে চালায়, তবে কখনো pass, কখনো fail — flaky test-এর সবচেয়ে চেনা উৎসগুলোর একটি (অংশ ৮)।

Test data management

Test data management-এর কয়েকটি কাজের নিয়ম:

  • প্রতিটি test নিজের data বানায় (Arrange ধাপে, পর্ব ৪৬); ভাগ করা বড় “seed database”-এ একজন একটি সারি বদলালে দশটি অচেনা test ভাঙে।
  • শেষে পরিষ্কার: ROLLBACK; যেখানে তা চলে না (কোড নিজেই commit করে, কয়েকটি connection), সেখানে টেবিল খালি করা বা প্রতিটি test-এ আলাদা schema.
  • অনন্য মান আর ছোট builder: একসাথে কয়েকটি test চললে স্থির id ধাক্কা খায়; an_order().paid() ধাঁচের helper শুধু প্রাসঙ্গিক মান দেখায় (পর্ব ৩৩-এর builder pattern).
  • আসল গ্রাহকের তথ্য নয়: কৃত্রিম data, বা অন্তত ব্যক্তিগত তথ্য মুছে ফেলা (anonymized) data.

৬ · Contract test — consumer-driven contract আর mock drift

ধরুন একটি mobile app একটি order API ব্যবহার করে। App-এর দল নিজেদের unit test-এ API-র একটি mock বসিয়েছে, যা ফেরত দেয় {"id": 7, "state": "PAID", "total": 450}. ছয় মাস পরে API-এর দল total-এর নাম বদলে amount করল, আর নিজেদের test নতুন নামেই লিখল। দুই দিকেই সবুজ, অথচ production-এ app-এ সব অর্ডারের দাম ফাঁকা। এই অবস্থার নাম mock drift: আসল dependency বদলে গেছে, mock পুরোনো আচরণ নকল করে চলেছে (পর্ব ৪৯-এ এই ঝুঁকির কথা এসেছিল)।

দর্জি গ্রাহকের মাপ — কাঁধ ১৭ ইঞ্চি — নিজের খাতায় লিখে রেখেছেন। গ্রাহকের ওজন কমেছে, কেউ জানায়নি। খাতা অনুযায়ী নিখুঁত জামা, অথচ গায়ে লাগে না: খাতার মাপ (mock) আসল মানুষের (provider) সাথে মেলানো হয়নি। Contract test মানে নিয়মিত দুই দিক মিলিয়ে দেখা।

সমাধানের নাম contract test, আর তার জনপ্রিয় রূপ consumer-driven contract: যে পক্ষ API ব্যবহার করে (consumer), সে নিজে লিখে দেয় কোন request পাঠাবে আর response-এর কোন অংশ পড়বে; provider সেই সব চুক্তি নিজের build-এ যাচাই করে — একে বলে provider verification. ধারণাটি Ian Robinson-এর ২০০৬ সালের একটি প্রবন্ধের সাথে যুক্ত; architecture-এর দিক থেকে আলোচনা System Design কোর্সের Microservices পর্বের অংশ ৯-এ। সবচেয়ে পরিচিত tool Pact (আনুমানিক ২০১৩ থেকে open source), যার “broker” চুক্তিগুলো এক জায়গায় রাখে; একই কাজের অন্য tool-ও আছে, যেমন Java-জগতে Spring Cloud Contract. পর্ব ৩১-এর (Interface আর contract) contract ছিল একটি function-এর চুক্তি; এখানে একই ধারণা network-এর দুই পারে।

নিচের খেলনা প্রোগ্রামে দুটি consumer — mobile app আর courier service — প্রত্যেকে শুধু নিজের পড়া field আর তাদের type লিখেছে। Provider-এর দুটি সংস্করণ যাচাই হচ্ছে:

# Toy consumer-driven contract check (real tools: Pact and similar).
# Each consumer records only the fields it READS and their types.
CONTRACTS = {
    "mobile-app": {"GET /orders/7": {"status": 200,
                   "body": {"id": int, "state": str, "total": int}}},
    "courier":    {"GET /orders/7": {"status": 200,
                   "body": {"id": int, "address": str}}},
}

def provider_v1(req):
    return 200, {"id": 7, "state": "PAID", "total": 450,
                 "address": "Mirpur 10, Dhaka", "items": 3}

def provider_v2(req):   # a developer renamed total -> amount
    return 200, {"id": 7, "state": "PAID", "amount": 450,
                 "address": "Mirpur 10, Dhaka", "items": 3, "eta_min": 40}

def verify(provider):
    ok = True
    for consumer, interactions in CONTRACTS.items():
        for req, exp in interactions.items():
            status, body = provider(req)
            problems = [] if status == exp["status"] else [f"status {status}"]
            for field, typ in exp["body"].items():
                if field not in body:
                    problems.append(f"missing field '{field}'")
                elif not isinstance(body[field], typ):
                    problems.append(f"'{field}' is not {typ.__name__}")
            ok = ok and not problems
            print(f"  {consumer:<11}{req}: " + ("PASS" if not problems else "FAIL " + "; ".join(problems)))
    return ok

for name, prov in [("v1", provider_v1), ("v2", provider_v2)]:
    print(f"provider {name}:")
    print("  can deploy?", "yes" if verify(prov) else "NO - a consumer would break")

আউটপুট:

provider v1:
  mobile-app GET /orders/7: PASS
  courier    GET /orders/7: PASS
  can deploy? yes
provider v2:
  mobile-app GET /orders/7: FAIL missing field 'total'
  courier    GET /orders/7: PASS
  can deploy? NO - a consumer would break

তিনটি জিনিস লক্ষ করুন। (১) চুক্তি শুধু পড়া field-এর: items কেউ চায় না, তাই provider সেটি বদলাতে স্বাধীন; v2-এর নতুন eta_min-ও কাউকে ভাঙায়নি, কারণ চুক্তি বাড়তি field-এ আপত্তি করে না। (২) মিল হচ্ছে type দিয়ে, নির্দিষ্ট মান দিয়ে নয় — Pact-এর মতো tool-এ একে বলে type matcher. (৩) নাম বদলে শুধু mobile app ভাঙল, courier নয় — provider-এর দল জানল ঠিক কার সাথে কথা বলতে হবে, আর জানল deploy-এর আগে। খেলনার সীমা: আসল tool header, query, nested JSON, array আর “provider state” (যেমন “order 7 আছে”) সামলায়।

এবার পুরো চক্রটি — consumer-এর test থেকে provider-এর deploy পর্যন্ত — ধাপে ধাপে:

mobile-app (consumer) ↔ order-api (provider)
কে কাজ করছে
কী ঘটছে
ফল
  1. Consumer-এর test. Mobile app-এর দল তাদের API client-এর test লেখে একটি mock provider-এর বিরুদ্ধে, যা Pact-ধাঁচের tool চালায়। Test বলে: “GET /orders/7 পাঠালে আমি id, state, total পড়ব।” এখনো এটি শুধু app-এর অনুমান।
  2. Contract ফাইল। পার্থক্য এখানে: tool mock-এর প্রতিটি request আর প্রত্যাশিত response রেকর্ড করে একটি JSON contract ফাইলে লেখে। অনুমানটি এখন লিখিত, যন্ত্রে পড়া যায় এমন চুক্তি।
  3. Broker-এ জমা। Contract-টি একটি ভাগ করা জায়গায় (broker) যায়, consumer-এর সংস্করণসহ — সাধারণত commit-এর hash, যেমন a1b2c3d. Courier service-এর দলও নিজের contract জমা দিয়েছে।
  4. Provider verification. Order API-এর CI broker থেকে সব consumer-এর contract আনে। প্রতিটি interaction-এর আগে “provider state” সাজায় (test database-এ order 7 রাখা), তারপর request-টি আসল provider কোডে চালিয়ে response মেলায়। দুটিই PASS.
  5. Breaking change ধরা পড়ল। একজন ডেভেলপার total-কে amount করলেন। Provider-এর নিজের unit test সবুজ, কিন্তু verification-এ mobile-app FAIL — merge-এর আগেই, production-এ নয়। এটিই mock drift-এর প্রতিষেধক: mock-এর অনুমান আর আসল provider-কে নিয়মিত মেলানো।
  6. Expand, তারপর contract. সমাধান নাম বদল বাতিল নয়, ধাপে ধাপে বদল: কিছুদিন দুটি field-ই পাঠানো (expand). App নতুন সংস্করণে amount পড়বে, নতুন contract জমা দেবে; যখন কোনো চালু consumer-এর contract-এ আর total নেই, তখন সেটি মোছা (contract). পর্ব ১৮-এর breaking change আর SemVer-এর আলোচনার বাস্তব রূপ।
  7. Deploy-এর আগে শেষ প্রশ্ন। Broker-এর একটি matrix রাখে কোন consumer-সংস্করণ কোন provider-সংস্করণের সাথে যাচাই হয়েছে। Pact-এর জগতে এই প্রশ্নের tool-এর নাম can-i-deploy (লেখার সময়ে): production-এ যে app-সংস্করণ চলছে, তার সাথে এই provider যাচাইকৃত কি না। হ্যাঁ হলে deploy.

Contract test provider-এর ব্যবসায়িক নিয়মের test নয় — শুধু বলে দুই পক্ষ একই ভাষায় কথা বলছে কি না। এটি সবচেয়ে কাজের, যখন consumer আর provider আলাদা দলের আর আলাদা সময়ে deploy হয়; একই দলের দুটি module-এর জন্য সাধারণত narrow integration test-ই যথেষ্ট।

৭ · End-to-end আর UI test — page object, আর কেন অল্প

E2E test পুরো system চালায় — আসল browser, আসল backend, আসল (test) database — আর ব্যবহারকারীর মতো ক্লিক আর টাইপ করে। Browser চালানোর পরিচিত tool: Selenium (২০০৪-এ শুরু; এর WebDriver পরে W3C-এর মান হয়), Cypress (২০১০-এর দশকের শেষে জনপ্রিয়), আর Microsoft-এর Playwright (২০২০)। কোনটি “সেরা”, তা বছর বছর বদলায়; নিচের ধারণাগুলো সবগুলোতেই খাটে।

Page object — UI-এর খুঁটিনাটি এক জায়গায়

E2E test-এর প্রধান রক্ষণাবেক্ষণ-খরচ: UI বদলায়। একটি বোতামের লেখা “Pay” থেকে “টাকা দিন” হলো, আর পঞ্চাশটি test-এ সেই বোতাম খোঁজার কোড ভাঙল। Page object pattern (Selenium সম্প্রদায়ে প্রচলিত; Fowler-এর ২০১৩ সালের একটি লেখায় বর্ণিত) প্রতিটি পাতা বা অংশের জন্য একটি class বানায়, যার method-গুলো ব্যবহারকারীর কাজের ভাষায় — payWithCard(), “এই CSS selector-এ ক্লিক” নয়। UI বদলালে শুধু page object বদলায়, test নয়। Playwright-ধাঁচে (লেখার সময়ের API):

// pages/checkout-page.js — the only place that knows the page's details
export class CheckoutPage {
  constructor(page) { this.page = page; }
  async open() { await this.page.goto('/checkout'); }
  async payWithCard(number) {
    await this.page.getByLabel('Card number').fill(number);
    await this.page.getByRole('button', { name: 'Pay' }).click();
  }
  confirmation() { return this.page.getByTestId('order-confirmation'); }
}

// tests/checkout.spec.js — reads like the user's journey
import { test, expect } from '@playwright/test';
import { CheckoutPage } from '../pages/checkout-page.js';

test('paying by card shows an order number', async ({ page }) => {
  const checkout = new CheckoutPage(page);
  await checkout.open();
  await checkout.payWithCard('0000 1111 2222 3333');      // test-only sandbox card
  await expect(checkout.confirmation()).toContainText('Order #');   // waits, no sleep
});

শেষ লাইনে expect(…).toContainText শর্ত মেলা পর্যন্ত একটি সময়সীমার মধ্যে বারবার দেখে — নির্দিষ্ট sleep নেই (কেন জরুরি, অংশ ৮-এ)। আর খোঁজা হচ্ছে getByTestId বা getByRole দিয়ে, CSS class দিয়ে নয়, যা design বদলালেই বদলায়। (async/await-এর ভিত্তি JavaScript · async/await-এ।)

E2E কম রাখার কৌশল

  • শুধু সবচেয়ে মূল্যবান যাত্রা: login, খোঁজা, কার্টে যোগ, টাকা দেওয়া — যা ভাঙলে ব্যবসা থামে। প্রতিটি validation বার্তা E2E-তে নয়; সেটি unit বা component test-এ।
  • Setup UI দিয়ে নয়: “অর্ডার বাতিল” test করতে প্রথমে UI দিয়ে অর্ডার বানাবেন না; API বা database দিয়ে সরাসরি বানিয়ে শুধু বাতিলের অংশটি UI-তে।
  • বাইরের service নকল: আসল payment gateway, SMS — এগুলোর sandbox বা service virtualization (network-এ সাড়া দেওয়া নকল server). নইলে অন্যের downtime আপনার build লাল করবে।
  • একটি ভুল, একটি স্তর: E2E-তে একটি bug ধরা পড়লে নিচের স্তরে একটি test লিখুন, যা সেটি ধরত। E2E-এর সংখ্যা না বাড়িয়ে নিরাপত্তা-জাল শক্ত হয়।

৮ · Flaky test — কারণ, শনাক্তকরণ, quarantine আর সমাধান

Flaky test কোড না বদলেও কখনো pass, কখনো fail করে। Google-এর testing blog-এ ২০১৬ সালের একটি লেখায় (John Micco) জানানো হয়েছিল, তাঁদের test run-এর প্রায় ১.৫% flaky ফল দেখাত, আর প্রায় ১৬% test-এ কিছু না কিছু flakiness ছিল — এক প্রতিষ্ঠানের সংখ্যা, অন্যত্র আলাদা হবে, কিন্তু সমস্যাটি যে বড় দলেও বড়, তা বোঝায়।

ঈদের আগে বাস কাউন্টারে আপনি বন্ধুকে বললেন, “ঠিক দশ মিনিট পরে আসবেন, টিকিট হাতে থাকবে।” বেশিরভাগ দিন দশ মিনিটেই হয়। কিন্তু ভিড়ের দিনে লাগে পনেরো — বন্ধু এসে খালি হাতে ফেরেন। দোষ টিকিট-বিক্রেতার নয়, “ঠিক দশ মিনিট” ধরে নেওয়ার। ঠিক উপায়: “টিকিট হাতে পেলেই ফোন দেব” — ঘড়ির বদলে ঘটনার জন্য অপেক্ষা। Flaky test-এর সবচেয়ে চেনা কারণও এটিই।

প্রধান কারণগুলো

কারণকেমন দেখায়প্রতিকার
সময় ও async অপেক্ষাsleep(200), তারপর একবার দেখা; ধীর যন্ত্রে failশর্তের জন্য অপেক্ষা (poll বা event), একটি উদার deadline-সহ; বা নকল ঘড়ি
ভাগ করা অবস্থাDatabase-এর সারি, global state, cache, ফাইল আগের test রেখে গেছেপ্রতিটি test নিজের data; rollback; নতুন object; অনন্য নাম
Test-এর ক্রমএকা pass, পুরো suite-এ fail (বা উল্টো)ক্রম এলোমেলো করে চালিয়ে ধরা; নির্ভরতা মোছা
বাইরের servicePayment sandbox ধীর বা বন্ধ, network-এর timeoutStub, service virtualization, contract test; আসলটি আলাদা ছোট suite-এ
সময়-অঞ্চল ও তারিখরাত ১২টার পরে বা মাসের শেষ দিনে fail; CI UTC-তে, ডেভেলপার Asia/Dhaka-য় (UTC+6)ঘড়ি inject করা; সময়-অঞ্চল স্পষ্ট; স্থির তারিখে test
এলোমেলো মানRandom data কখনো সীমার মান দেয়Seed স্থির, বা fail হলে seed ছাপা যাতে পুনরুৎপাদন যায় (PRNG)
Concurrencyদুটি thread-এর ক্রম প্রতিবার আলাদাRace condition খুঁজে ঠিক করা — এটি প্রায়ই production-এর আসল bug

গবেষণাও এই তালিকার সাথে মেলে: Qingzhou Luo ও সহকর্মীদের “An Empirical Analysis of Flaky Tests” (FSE ২০১৪) কয়েকটি বড় open-source project-এর flaky test ঠিক করার commit বিশ্লেষণ করে মূল কারণের সবচেয়ে বড় তিনটি ভাগ হিসেবে পেয়েছিল async অপেক্ষা, concurrency আর test-এর ক্রমের ওপর নির্ভরতা। শেষ সারিটি মনে রাখুন: সব flaky test “test-এর দোষ” নয় — কখনো কোডে সত্যিই একটি race আছে, তাই মুছে দেওয়ার আগে কারণ খুঁজুন।

শনাক্তকরণ — আবার চালানোর পরিসংখ্যান

একটি test flaky কি না, তার সবচেয়ে সরল প্রমাণ: একই কোডে বারবার চালিয়ে দুই রকম ফল। নিচের প্রোগ্রামে একটি কাল্পনিক payment callback ৬০ থেকে ২৬০ মিলিসেকেন্ডের মধ্যে আসে (লোড অনুযায়ী)। প্রথম test ঠিক ২০০ মিলিসেকেন্ড ঘুমিয়ে একবার দেখে; দ্বিতীয়টি শর্তের জন্য অপেক্ষা করে। সময়টি নকল — আসলে কেউ ঘুমায় না — আর এলোমেলো দেরি আসে একটি seed-যুক্ত PRNG (mulberry32) থেকে, তাই ফল প্রতিবার একই:

// Flaky-test detector on SIMULATED time (no real waiting). mulberry32 = seeded PRNG.
function mulberry32(a) {
  return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const rand = mulberry32(51);
// The system under test: the payment callback arrives after 60..260 ms (varies by load).
const callbackDelay = () => 60 + Math.floor(rand() * 200);

// FLAKY: sleeps a fixed 200 ms, then checks once.
function testSleepThenCheck() {
  const arrivesAt = callbackDelay();
  return arrivesAt <= 200;                      // was the order PAID at t = 200 ms?
}
// FIXED: waits for the condition itself, polling every 10 ms, up to a 2000 ms deadline.
function testWaitForCondition() {
  const arrivesAt = callbackDelay();
  for (let t = 0; t <= 2000; t += 10) if (t >= arrivesAt) return true;
  return false;                                  // only a real hang fails
}

function rerun(test, n) {
  let pass = 0;
  for (let i = 0; i < n; i++) if (test()) pass++;
  const verdict = pass === n ? "stable pass" : pass === 0 ? "stable fail (real bug?)" : "FLAKY";
  console.log(`${test.name.padEnd(21)} ${pass}/${n} passed (${(100 * pass / n).toFixed(0)}%) -> ${verdict}`);
  return pass / n;
}
const p = rerun(testSleepThenCheck, 50);
console.log(`  chance a red run turns green on one retry: ${(100 * p).toFixed(0)}%`);
console.log(`  chance 3 runs in a row all pass: ${(100 * p ** 3).toFixed(0)}%`);
rerun(testWaitForCondition, 50);

আউটপুট:

testSleepThenCheck    36/50 passed (72%) -> FLAKY
  chance a red run turns green on one retry: 72%
  chance 3 runs in a row all pass: 37%
testWaitForCondition  50/50 passed (100%) -> stable pass

৫০ বারে ৩৬ বার pass, ১৪ বার fail — একই কোড, দুই রকম ফল, তাই FLAKY. নিয়মটি সরল: সব pass মানে স্থির, সব fail মানে সম্ভবত সত্যিকারের ভুল, মাঝামাঝি মানে flaky. দ্বিতীয় লাইনটি বিপদের কথা: লাল হলে একবার আবার চালালেই ৭২% সম্ভাবনায় সবুজ — তাই দল অভ্যাসে “rerun” চাপে, আর কেউ কারণ খোঁজে না। তৃতীয় লাইন: পরপর তিনবার সবুজের সম্ভাবনা মাত্র ৩৭% (০.৭২৩ ≈ ০.৩৭৩)। ঠিক করা test একই এলোমেলো দেরিতে ৫০/৫০: সে ঘড়ি নয়, ঘটনা ধরে অপেক্ষা করে। ২০০০ মিলিসেকেন্ডের deadline শুধু সত্যিকারের আটকে যাওয়া ধরতে; শর্ত মিললেই loop থামে, তাই দ্রুত যন্ত্রে test দ্রুতই শেষ। আরেকটি পথ: ঘড়ি inject করে test-এ নকল ঘড়ি এগিয়ে দেওয়া।

বাস্তবে এই পরিসংখ্যান আসে CI-এর ইতিহাস থেকে: একই commit-এ একবার লাল, একবার সবুজ মানে flaky; অনেক CI platform এমন হিসাব নিজেই দেখায় (platform-ভেদে আলাদা)। নিজে খুঁজতে test-টি লুপে শতবার, এলোমেলো ক্রমে আর কৃত্রিম চাপের মধ্যে চালান।

Quarantine — আর retry-এর ফাঁদ

Flaky test খুঁজে পাওয়ার পরের প্রশ্ন: ঠিক করার আগ পর্যন্ত কী করবেন? দুটি সহজ কিন্তু খারাপ পথ — মুছে ফেলা (যা পাহারা দিত, তা হারাল), আর স্বয়ংক্রিয় retry দিয়ে চুপ করানো (অনুশীলন ১-এ দেখবেন, এক retry-তে মিথ্যা লাল প্রায় শূন্য — এবং ঠিক সেজন্যই কেউ আর কারণ খোঁজে না, আর আসল race-এর bug-ও retry-তে লুকিয়ে যায়)। মাঝের পথ quarantine: test-টি চলতে থাকে আর ফল রেকর্ড হয়, কিন্তু merge আটকায় না — আর সাথে থাকে একজন মালিক, একটি ticket আর একটি সময়সীমা।

# pytest.ini — register a custom marker
[pytest]
markers =
    quarantine: known flaky; owner and ticket in the reason

# test_payment.py
@pytest.mark.quarantine  # owner: Karima, ticket SHOP-412, fix by 2026-11-15
def test_payment_confirmation_appears():
    ...

# CI: the blocking job skips them; a separate non-blocking job runs only them
#   pytest -m "not quarantine"
#   pytest -m quarantine

এখন পুরো রোগনির্ণয় — CI-তে প্রথম লাল থেকে test-টি মূল suite-এ ফেরা পর্যন্ত:

test_payment_confirmation_appears
ধাপ
প্রমাণ
সিদ্ধান্ত
  1. লাল build. একটি README-বদলের PR-এর পরে main লাল — payment confirmation-এর test. “Rerun”-এ সবুজ। README payment ভাঙতে পারে না, তাই সন্দেহ flakiness-এ; কিন্তু একবারের সবুজ প্রমাণ নয়।
  2. ইতিহাস দেখা। শেষ ৫০টি run-এ ৩৬ pass, ১৪ fail, একই commit-এ দুই রকম ফল — flaky নিশ্চিত, আর সমস্যা আগে থেকেই ছিল; শেষ PR-এর লেখকের দোষ নয়।
  3. পুনরুৎপাদন। নিজের কম্পিউটারে লুপে ৫০ বার: ১৪ বার fail; পাশে ভারী কাজ চালালে আরও বেশি — সময়-নির্ভর কারণের জোরালো ইঙ্গিত।
  4. Quarantine. কারণ খুঁজতে সময় লাগবে, ততক্ষণ সবার merge আটকানো অন্যায়। Marker — মালিক Karima, ticket SHOP-412, সময়সীমা ১৫ নভেম্বর; test চলছে আলাদা non-blocking job-এ।
  5. কারণ খোঁজা। Fail হওয়া run-গুলোর log: প্রতিবার callback এসেছে ২০০ ms-এর পরে, আর test-এ sleep(200), তারপর একবার দেখা — বাসের টিকিটের “দশ মিনিট পরে আসবেন”।
  6. সমাধান। Sleep-এর বদলে শর্তের অপেক্ষা: PAID না হওয়া পর্যন্ত প্রতি ১০ ms-এ দেখা, সর্বোচ্চ ২ সেকেন্ড (বা নকল ঘড়ি)। sleep(500) সমাধান নয় — ভুলটি শুধু বিরল হতো, আর প্রতিটি run ধীর।
  7. যাচাই আর ফেরত। স্থানীয় ৫০/৫০, CI-তে ২০০ run-এ একটিও fail নেই। Marker সরিয়ে মূল suite-এ ফেরত, আর review-এর চেকলিস্টে নতুন লাইন: “test-এ নির্দিষ্ট সময়ের sleep আছে কি?”

Quarantine-এর তালিকা কবরস্থান হয়ে যেতে পারে — সময়সীমা ছাড়া test সেখানে মাসের পর মাস পড়ে থাকে, আর যে ঝুঁকি পাহারা দিত, তা পাহারাহীন। সময়সীমা পেরোলে দুটি সৎ বিকল্প: ঠিক করা, অথবা ভেবেচিন্তে মুছে ফেলে একই ঝুঁকি নিচের স্তরের একটি স্থির test দিয়ে ঢাকা।

৯ · Suite-এর গতি, parallelism, test environment আর CI

গতি — কেন মিনিট গুরুত্বপূর্ণ

Suite ২ মিনিটে চললে ডেভেলপার ফল দেখেই এগোন; ২০ মিনিটে চললে অন্য কাজে চলে যান, আর commit-এর আগে স্থানীয়ভাবে চালানো ছেড়ে দেন। দ্রুত করার প্রধান উপায়:

  • ধীর test খুঁজুন: বেশিরভাগ test runner সবচেয়ে ধীর test-এর তালিকা দেখাতে পারে (যেমন pytest-এর --durations=10). প্রায়ই অল্প কয়েকটি test মোট সময়ের বড় অংশ নেয় — অংশ ২-এ ২০টি E2E নিয়েছিল ৮০%.
  • নিচে নামান: একটি E2E যা আসলে একটি হিসাবের নিয়ম যাচাই করে, তাকে unit test-এ নামান।
  • ধীর সাজানো ভাগ করুন: container বা browser প্রতিটি test-এ নতুন নয়, একবার; data-র পরিষ্কার rollback দিয়ে।
  • Parallelism আর sharding: suite কয়েকটি ভাগে কয়েকটি process বা যন্ত্রে। শর্ত: test-গুলো hermetic — নিজের data, নিজের port, ভাগ করা অবস্থা নেই। অংশ ৫-এর order-dependent test ঠিক এখানেই ধরা পড়ে।
  • শুধু প্রভাবিত test: বড় repository-তে dependency graph দেখে শুধু ছোঁয়া test (পর্ব ১৯-এর affected build); দিনে অন্তত একবার পুরো suite.

Parallelism-এর সীমা: মোট সময় সবচেয়ে লম্বা ভাগের সমান, আর সবচেয়ে লম্বা একক test-এর চেয়ে কম কখনো নয়। অংশ ২-এর ice-cream cone ৮টি যন্ত্রেও ১২.৬ মিনিট — ৮ গুণ যন্ত্রের খরচে।

Test environment

একটি test environment হলো যন্ত্র, software-এর সংস্করণ, config আর data-র পুরো সমষ্টি। সাধারণ স্তর: ডেভেলপারের নিজের কম্পিউটার → CI-এর ক্ষণস্থায়ী পরিবেশ (প্রতিটি run নতুন করে তৈরি) → staging (production-এর যতটা কাছাকাছি সম্ভব) → production. দুটি নীতি:

  • ক্ষণস্থায়ী আর পুনরুৎপাদনযোগ্য: প্রতিটি test run নিজের নতুন পরিবেশ পায় (container, script দিয়ে তৈরি)। একটি ভাগ করা staging-এ দশটি দলের E2E একসাথে চললে তারা একে অপরের data বদলায় — flaky test-এর কারখানা।
  • Production-এর সাথে পার্থক্য সচেতনভাবে: database-এর সংস্করণ, সময়-অঞ্চল, ভাষা-সেটিং, feature flag — যত পার্থক্য, তত “test-এ ঠিক, production-এ ভুল”। কোনগুলো ইচ্ছাকৃত, তার তালিকা রাখুন। পরিবেশ আর deployment pipeline-এর বিস্তারিত পর্ব ৫৯-এ।

CI-তে test — একটি ধাপের ক্রম

CI pipeline সাধারণত দ্রুত আর সস্তা check আগে চালায়, যাতে সবচেয়ে সাধারণ ভুল সবচেয়ে আগে ধরা পড়ে: static check আর unit test (মিনিটখানেক) → narrow integration আর contract test → কয়েকটি E2E smoke test → merge-এর পরে বা রাতে পুরো E2E আর ধীর suite. প্রতিটি ধাপ ব্যর্থ হলে পরেরটি চলে না — “fail fast”. Pipeline-এর নকশা, cache আর CI YAML পর্ব ৫৮-এ।

আরও গভীরে: Software testing-এর আন্তর্জাতিক মানের ধারা ISO/IEC/IEEE 29119 (২০১৩ থেকে কয়েক খণ্ডে) test process, নথি আর কৌশলের শব্দভান্ডার দেয়; integration-এর কৌশলগুলোর পাঠ্যবই-রূপ Pressman আর Sommerville-এর বইয়ে। Software Engineering at Google (২০২০) বইয়ের testing অধ্যায়গুলো test-এর আকার, hermetic test আর বিশাল suite চালানোর একটি প্রকাশিত বিবরণ।

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

  • Test pyramid কী, আর কেন? — নিচে অনেক unit, মাঝে কম integration/service, ওপরে অল্প E2E/UI; কারণ নিচের test দ্রুত, সস্তা, স্থির আর নির্দিষ্ট feedback দেয় (Mike Cohn, Succeeding with Agile, ২০০৯)।
  • Top-down আর bottom-up integration-এর পার্থক্য? — Top-down ওপর থেকে শুরু, নিচের অনুপস্থিত module-এর জায়গায় stub; bottom-up নিচ থেকে, ওপরের ডাকার module-এর জায়গায় driver. Sandwich দুটি মেশায়; big-bang সব একবারে।
  • Contract test কোন সমস্যা মেটায়? — Mock আর আসল provider-এর মধ্যে পার্থক্য (mock drift); consumer-এর লিখিত চুক্তি provider-এর build-এ যাচাই হয়, তাই breaking change deploy-এর আগে ধরা পড়ে।
  • Flaky test পেলে কী করবেন? — পরিসংখ্যান দিয়ে নিশ্চিত, পুনরুৎপাদন, মালিক-সময়সীমাসহ quarantine, মূল কারণ (অপেক্ষা, ভাগ করা অবস্থা, ক্রম, সময়, বাইরের service) ঠিক করা; অন্ধ retry নয়।
  • In-memory database-এ integration test-এর ঝুঁকি? — Production-এর engine থেকে SQL, type আর transaction-এর আচরণে পার্থক্য; container-এ একই engine চালানো (যেমন Testcontainers) নিরাপদ।

১০ · AI-এর যুগে — AI-লেখা test, আর test-কৌশলে AI

AI দিয়ে দ্রুত অনেক test লেখানো যায় — আর ঠিক সেখানেই ঝুঁকি। Test-এর সংখ্যা বাড়ে, কিন্তু pyramid-এর আকার, flakiness আর চুক্তির সত্যতা AI নিজে থেকে দেখে না।

AI-এর চেনা ভুলকীভাবে ধরবেন
E2E test-এ sleep(2000) বা waitForTimeout — “এখন তো চলছে”শর্তের জন্য অপেক্ষা; test-টি লুপে ৫০ বার, চাপের মধ্যে চালিয়ে দেখা
সব কিছু mock করা test, যা implementation-এর হুবহু প্রতিচ্ছবি — কিছুই যাচাই করে নাকোডে ইচ্ছে করে ভুল ঢুকিয়ে দেখা test fail করে কি না (mutation testing, পর্ব ৫০)
Mock-এর response বানানো, আসল API-র সাথে না মিলিয়ে — mock drift জন্মের সময়েইResponse-এর আকার আসল API-র নথি বা contract থেকে নেওয়া; contract test
Flaky test “ঠিক” করতে retry বা লম্বা timeout যোগআগে কারণ জিজ্ঞেস করুন; অংশ ৮-এর তালিকা ধরে খোঁজা
বানানো library API বা flag (Testcontainers, Playwright, Pact-এর method)অফিসিয়াল documentation; নিজে চালিয়ে দেখা
Test data-য় আসল দেখতে নাম-ফোন-ঠিকানা, বা prompt-এ production-এর dataকৃত্রিম data; prompt-এ গ্রাহকের তথ্য বা secret নয়
Test ঠিক করতে git reset --hard বা git push --force-এর পরামর্শ, সতর্কতা ছাড়াThrowaway clone-এ আগে চালানো; শুধু নিজের branch-এ --force-with-lease

AI-কে test-কৌশলের সহকারী হিসেবে ব্যবহারের একটি prompt — এতে স্তর, ঝুঁকি আর “অনুমান হলে বলো” নির্দেশ আছে:

তুমি একজন সতর্ক test engineer. নিচে একটি কাল্পনিক অ্যাপের architecture আর প্রধান user journey দিলাম (কোনো secret বা গ্রাহকের তথ্য নেই)। [component-এর তালিকা, কে কাকে ডাকে, বাইরের service] [৩–৫টি প্রধান user journey] ১. প্রতিটি ঝুঁকির জন্য বলো কোন সবচেয়ে নিচের স্তরে সেটি ধরা যায়: unit, narrow integration, contract, না E2E — আর কেন। ২. E2E test সর্বোচ্চ ৫টি রাখো; প্রতিটির জন্য বলো কোন journey, আর setup কীভাবে UI ছাড়া হবে। ৩. কোথায় flaky হওয়ার ঝুঁকি (অপেক্ষা, ভাগ করা data, সময়-অঞ্চল, বাইরের service), আর প্রতিটির প্রতিরোধ। ৪. কোনো tool-এর API বা flag উল্লেখ করলে "documentation-এ যাচাই করুন" লিখবে; নিশ্চিত না হলে বানাবে না। ৫. sleep বা অন্ধ retry প্রস্তাব করবে না।

AI-এর সাহায্যে test-কৌশল — চেকলিস্ট

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

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

প্র ০১Test pyramid দশকের পর দশক ধরে পরামর্শ, অথচ testing trophy-র সমর্থকেরা বলেন “mostly integration”. কে ঠিক? দুটি পরামর্শ কি সত্যিই পরস্পরবিরোধী?

দুটি পরামর্শের জন্ম দুটি আলাদা প্রেক্ষাপটে। Mike Cohn যখন ২০০০-এর দশকে pyramid-এর কথা বলছিলেন, তখন অনেক দলে automation মানেই ছিল UI recording tool — কেউ একবার ক্লিক করে রেকর্ড করতেন, tool সেটি বারবার চালাত। এই test-গুলো ছিল অসম্ভব ধীর আর ভঙ্গুর: একটি বোতাম সরলেই শত test ভাঙত। Pyramid-এর বার্তা ছিল প্রতিক্রিয়া: test-এর ভার নিচে নামান, যেখানে তা দ্রুত আর স্থির।

প্রায় এক দশক পরে frontend জগতের অভিযোগ ছিল উল্টো। React-এর মতো framework-এ অনেকে প্রতিটি component-কে সম্পূর্ণ আলাদা করে, ভেতরের state আর method ধরে test লিখছিলেন। এমন test implementation-এর সাথে বাঁধা: কোড পুনর্গঠন (refactor) করলেই ভাঙে, অথচ ব্যবহারকারীর অভিজ্ঞতা একই। Kent C. Dodds-এর যুক্তি ছিল, যে test সফটওয়্যারকে ব্যবহারকারীর মতো করে চালায়, তা বেশি আস্থা দেয় — আর আধুনিক tool-এ কয়েকটি component একসাথে, browser ছাড়াই, দ্রুত চালানো যায়। তাই trophy-র মোটা “integration” অংশ আসলে বেশ দ্রুত।

এখানেই বিরোধ অনেকটা মিলিয়ে যায়। দুই পক্ষই একই দুই মাপকাঠি মানে: গতি আর আস্থা। Pyramid বলে “ধীর, ভঙ্গুর test কম”; trophy বলে “implementation-এ বাঁধা test কম”। একটি দ্রুত, স্থির, sociable test — যা কয়েকটি class একসাথে চালায় — কেউ বলবেন unit, কেউ integration. Fowler এই কারণে solitary আর sociable unit test-এর পার্থক্যের কথা লিখেছেন। আসল বিরোধ নাম নিয়ে, নীতি নিয়ে কম।

তবু প্রেক্ষাপট গুরুত্বপূর্ণ। একটি কর-হিসাবের engine, যেখানে শত শত নিয়ম আর boundary — সেখানে খুঁটিনাটি unit test-ই সবচেয়ে কার্যকর। একটি form-ভরা admin panel, যেখানে যুক্তি কম আর জোড়া বেশি — সেখানে trophy যুক্তিসংগত। এবং দুই ক্ষেত্রেই browser-চালানো E2E অল্প।

মূল উপলব্ধি: আকার একটি ঝুঁকির মানচিত্রের ছবি, ধর্মগ্রন্থ নয়। প্রশ্ন করুন: আমার system-এ ভুল কোথায় জন্মায়, আর সেটি সবচেয়ে দ্রুত আর স্থিরভাবে কোন স্তরে ধরা যায়?

প্র ০২১৯৯৯ সালে NASA-র Mars Climate Orbiter হারিয়ে যায় দুটি software-এর মধ্যে এককের অমিলে। এটি কোন স্তরের testing-এর ব্যর্থতা? আজকের contract test এমন ভুল ধরতে পারত কি?

Mars Climate Orbiter ১৯৯৯ সালের সেপ্টেম্বরে মঙ্গলের কক্ষপথে ঢোকার সময় হারিয়ে যায়। NASA-র তদন্ত বোর্ডের প্রকাশিত প্রতিবেদন অনুযায়ী মূল কারণ: মাটিতে থাকা একটি software (মহাকাশযান নির্মাতা প্রতিষ্ঠানের দিকের) thruster-এর impulse-এর মান দিচ্ছিল ইংরেজি এককে (pound-force second), অথচ নথিভুক্ত interface আর navigation দলের software ধরে নিয়েছিল মেট্রিক একক (newton second). পার্থক্য প্রায় সাড়ে চার গুণ। মাসের পর মাস ছোট ভুল জমে কক্ষপথের হিসাব সরে যায়, আর মহাকাশযানটি পরিকল্পনার চেয়ে অনেক নিচ দিয়ে মঙ্গলের বায়ুমণ্ডলে ঢোকে বলে ধারণা করা হয়। প্রতিবেদন শুধু এককের ভুল নয়, যাচাই আর যোগাযোগের প্রক্রিয়ার দুর্বলতার কথাও বলেছিল — সন্দেহজনক সংকেত দেখেও তা যথেষ্ট গুরুত্ব পায়নি।

এটি একটি ক্লাসিক integration-এর ব্যর্থতা। প্রতিটি software নিজে নিজের প্রত্যাশা মেনে হয়তো ঠিকই চলছিল — অর্থাৎ নিজের unit test-এ সবুজ। ভুল ছিল সীমানায়: দুই পক্ষ চুক্তিটি আলাদাভাবে পড়েছিল। অংশ ৪-এর stepper-এর টাকা বনাম পয়সা এই ঘটনারই ছোট রূপ।

আজকের contract test কি ধরত? আংশিক। অংশ ৬-এর মতো type-ভিত্তিক চুক্তি দুটি সংখ্যার পার্থক্য ধরত না — দুটিই তো সংখ্যা। ধরতে হলে চুক্তিতে এককটি স্পষ্ট থাকতে হয়: field-এর নামে (impulse_newton_seconds), আলাদা একক-field-এ, বা type-এ (অনেক ভাষায় একক-সচেতন type বানানো যায়)। তারপর একটি উদাহরণ-ভিত্তিক চুক্তি — “এই input-এ উত্তর মোটামুটি এত” — একক ভুল হলে সাড়ে চার গুণ পার্থক্যে ধরা পড়ত। আর একটি broad integration বা end-to-end সিমুলেশন, যেখানে দুই দলের আসল software একসাথে চলে আর ফল স্বাধীন হিসাবের সাথে মেলানো হয়।

মূল উপলব্ধি: সবুজ unit test প্রমাণ করে প্রতিটি অংশ নিজের ধারণা মানছে, ধারণাগুলো মিলছে কি না নয়। সীমানার ভুল ধরতে সীমানায় test লাগে — আর চুক্তিতে অর্থ (একক, সময়-অঞ্চল, মুদ্রা) স্পষ্ট লিখতে হয়।

প্র ০৩Ariane 5-এর প্রথম উড্ডয়ন (১৯৯৬) ব্যর্থ হয়েছিল Ariane 4 থেকে নেওয়া software-এ। তদন্তে testing নিয়ে কী বলা হয়েছিল, আর “পুরোনো, পরীক্ষিত কোড” পুনর্ব্যবহারে এ থেকে কী শেখার আছে?

৪ জুন ১৯৯৬, Ariane 5-এর প্রথম উড্ডয়ন (Flight 501) উৎক্ষেপণের প্রায় ৪০ সেকেন্ড পরে পথ হারিয়ে ধ্বংস হয়। Jacques-Louis Lions-এর নেতৃত্বে তদন্ত বোর্ডের প্রকাশিত প্রতিবেদন অনুযায়ী, inertial reference system-এর software-এ একটি ৬৪-bit floating-point মানকে ১৬-bit signed integer-এ রূপান্তরের সময় overflow হয়। মানটি ছিল horizontal velocity-সম্পর্কিত একটি হিসাব। Ariane 4-এর পথে এই মান কখনো এত বড় হতো না, তাই সেখানে রূপান্তরটি নিরাপদ ছিল; Ariane 5 অনেক দ্রুত, তার পথে মান সীমা ছাড়িয়ে গেল। Exception-এ প্রধান আর backup দুটি যন্ত্রই একই কোড চালাচ্ছিল বলে দুটিই বন্ধ হলো। আরও দুঃখের: এই হিসাবটির উড্ডয়নের পরে আর কোনো কাজই ছিল না — Ariane 4-এর একটি সুবিধার জন্য রেখে দেওয়া।

Testing নিয়ে প্রতিবেদনের মূল পর্যবেক্ষণ ছিল, Ariane 5-এর আসল পথের data দিয়ে এই যন্ত্রকে পুরো system-এর সাথে closed-loop সিমুলেশনে যাচাই করা হয়নি; specification-এ Ariane 5-এর পথের তথ্যও ছিল না। অর্থাৎ প্রতিটি অংশ নিজের পুরোনো প্রেক্ষাপটে পরীক্ষিত ছিল, কিন্তু নতুন প্রেক্ষাপটে জোড়া লাগানো অবস্থায় নয় — integration আর system স্তরের ফাঁক।

পুনর্ব্যবহারের শিক্ষা গভীর। “এই কোড বছরের পর বছর নির্ভুল চলেছে” — কথাটি সত্য, কিন্তু শুধু সেই input-এর পরিসরে, যা সে দেখেছে। নতুন system-এ input-এর পরিসর বদলালে পুরোনো প্রমাণ আর খাটে না। পর্ব ৩১-এর design by contract-এর ভাষায়: কোডটির precondition (মান ১৬-bit-এ আঁটবে) কোথাও স্পষ্ট লেখা বা যাচাই করা ছিল না, তাই নতুন caller সেটি ভাঙল আর কেউ জানল না।

আজকের software-এ একই ধরনের ঘটনা ঘটে ছোট আকারে: একটি পুরোনো library নতুন দেশের মুদ্রা, নতুন সময়-অঞ্চল বা দশ গুণ বড় data-য় ব্যবহার। প্রতিকার একই — নতুন প্রেক্ষাপটের আসল ধরনের data দিয়ে integration test, boundary-র test (পর্ব ৪৭), আর ধারণাগুলো চুক্তিতে লেখা।

মূল উপলব্ধি: Test-এর প্রমাণ প্রেক্ষাপটের সাথে বাঁধা। কোড পুনর্ব্যবহার করলে পুরোনো test-এর ফল উত্তরাধিকার হিসেবে আসে না — নতুন জোড়ায়, নতুন data দিয়ে আবার যাচাই করতে হয়।

প্র ০৪একটি দলের CI-তে প্রায় প্রতিদিন কয়েকটি flaky test লাল হয়, আর সবাই অভ্যাসে “rerun” চাপেন। একজন প্রস্তাব দিলেন: “সব fail হওয়া test নিজে থেকে তিনবার পর্যন্ত আবার চলুক।” এর লাভ-ক্ষতি কী?

লাভটি তাৎক্ষণিক আর বাস্তব। অনুশীলন ১-এর হিসাবে দেখবেন, একবার retry-তেই মিথ্যা লাল ১৬% থেকে প্রায় ০.০৪%-এ নামে, কারণ একটি test-এর পরপর দুবার flaky হওয়ার সম্ভাবনা p২. দল আর অকারণে আটকায় না, CI-এর লাল আবার গুরুত্ব পায়। অনেক বড় প্রতিষ্ঠানও কোনো না কোনো রূপে retry ব্যবহার করে বলে প্রকাশিত লেখায় জানা যায় — Google-এর ২০১৬ সালের testing blog-এর লেখাতেও flaky test সামলানোর কৌশলের মধ্যে আবার চালানোর কথা এসেছিল।

কিন্তু ক্ষতিগুলো ধীরে ধীরে জমে। প্রথমত, retry একটি তথ্য মুছে দেয়: কোন test কতবার flaky হচ্ছে। Flakiness বাড়তে থাকে, কেউ টের পায় না, যতক্ষণ না একটি test তিনবারেও pass না করে। দ্বিতীয়ত, কিছু flaky test আসলে কোডের race condition বা timeout-এর bug দেখাচ্ছে — যা production-এ ব্যবহারকারীর সাথেও ঘটে। Retry ঠিক সেই সংকেত চাপা দেয়। তৃতীয়ত, retry সময় নেয়: ধীর E2E তিনবার চললে pipeline লম্বা হয়। চতুর্থত, এটি সংস্কৃতিতে বার্তা দেয় যে flaky test স্বাভাবিক, মেনে নেওয়ার জিনিস।

তাই প্রস্তাবটি গ্রহণযোগ্য একটি শর্তে: retry হবে স্বচ্ছ। যে test retry-তে pass করল, সেটি “passed with retry” হিসেবে রেকর্ড হবে, একটি ড্যাশবোর্ডে flakiness-এর হার দেখাবে, আর একটি সীমা পেরোলে test-টি নিজে থেকে quarantine-এ যাবে একজন মালিক আর ticket-সহ। Retry তখন একটি সাময়িক ব্যান্ডেজ, স্থায়ী চিকিৎসা নয়। আর দল প্রতি sprint-এ flaky test ঠিক করার জন্য নির্দিষ্ট সময় রাখবে — technical debt-এর মতোই (পর্ব ৪৩)।

একটি ব্যতিক্রমও ভাবুন: যে test বাইরের, নিয়ন্ত্রণের বাইরের service ছোঁয় (একটি sandbox payment gateway), সেখানে কিছু retry যুক্তিসংগত হতে পারে — তবে আরও ভালো সমাধান সেই নির্ভরতা মূল suite থেকে সরিয়ে contract test আর service virtualization-এ নেওয়া।

মূল উপলব্ধি: Retry লক্ষণ ঢাকে, রোগ সারায় না। এটি ব্যবহার করুন শুধু মাপা, দৃশ্যমান আর সময়সীমা-যুক্ত অবস্থায় — যাতে flakiness-এর সংখ্যাটি চোখের সামনে থাকে।

প্র ০৫একটি দল দ্রুততার জন্য repository-র integration test চালায় SQLite in-memory-তে, যদিও production-এ PostgreSQL. কয়েক মাস ভালো চলল, তারপর একটি bug production-এ গেল যা test-এ ধরা পড়েনি। কী কী ধরনের পার্থক্য এমন ফাঁক তৈরি করে, আর সমাধান কী?

SQLite অসাধারণ একটি database, কিন্তু PostgreSQL নয়। কয়েক ধরনের পার্থক্য বারবার ফাঁক তৈরি করে। Type-এর কড়াকড়ি: SQLite-এর সাধারণ টেবিলে একটি INTEGER কলামে লেখা ঢোকানো যায় (type affinity); PostgreSQL সেটি প্রত্যাখ্যান করে। তাই test-এ যে ভুল চুপচাপ পার হয়, production-এ error. SQL-এর রূপ: তারিখ-সময়ের function, ILIKE, JSON operator, upsert-এর রূপ, window function-এর কিছু রূপ — এক database-এর কোড আরেকটিতে চলে না বা অন্য ফল দেয়। অনেক দল তখন test-এর জন্য আলাদা query লেখে — তখন production-এর query আর test-ই হয় না। Constraint আর transaction: foreign key-এর যাচাই SQLite-এ একটি সেটিং দিয়ে চালু করতে হয়; isolation আর locking-এর আচরণ আলাদা, তাই concurrency-র bug একটিতে দেখা যায়, অন্যটিতে নয়। Collation আর case: বাংলা বা ইংরেজি লেখার তুলনা আর সাজানোর নিয়ম আলাদা হতে পারে। Migration: production-এর migration script SQLite-এ চলে না, তাই test-এর schema আলাদাভাবে বানানো — আর দুটি schema ধীরে ধীরে আলাদা হয়ে যায়।

সমাধানের প্রধান পথ: test-এ production-এর একই engine আর সংস্করণ, container-এ — Testcontainers বা CI-এর service container দিয়ে। আপত্তি সাধারণত গতি নিয়ে, কিন্তু অংশ ৫-এর কৌশল খরচ অনেক কমায়: container একবার পুরো session-এ, প্রতিটি test-এ transaction আর rollback, আর একসাথে কয়েকটি worker-এ আলাদা database. Migration-ও একই script দিয়ে, যাতে migration নিজেও test হয়।

তাহলে কি in-memory database অর্থহীন? না। Domain-এর নিয়মের test-এ database-ই লাগে না — সেখানে একটি fake repository (পর্ব ৪৯) সবচেয়ে দ্রুত। আর SQLite যেখানে production-এরও database (mobile app, ছোট desktop tool), সেখানে তো এটিই আসল। নীতিটি হলো: যে test database-এর আচরণ যাচাই করতে চায়, সে আসল database ব্যবহার করবে; যে test শুধু যুক্তি যাচাই করে, সে database-ই ছোঁবে না। মাঝামাঝি “প্রায়-আসল” database দুই দিকেরই খারাপটুকু দেয় — পুরো দ্রুতও নয়, পুরো বিশ্বস্তও নয়।

মূল উপলব্ধি: Integration test-এর পুরো মূল্য আসলের সাথে মিলে। নকল যত আসল থেকে দূরে, test তত কম প্রমাণ করে — আর ফাঁকটি দেখা যায় শুধু production-এ।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. হিসাব (suite-এর সময় আর মিথ্যা লাল): একটি কাল্পনিক বাজার অ্যাপের suite-এ ১২০০টি unit test (প্রতিটি ০.০০৪ সেকেন্ড, flaky নয়), ১৫০টি integration test (প্রতিটি ০.৮ সেকেন্ড, প্রতিটির flaky-সম্ভাবনা ০.০০০৫) আর ২৫টি E2E test (প্রতিটি ২৪ সেকেন্ড, flaky-সম্ভাবনা ০.০০৪)। flake-গুলো স্বাধীন ধরুন। (ক) এক যন্ত্রে মোট সময় কত? ৪টি যন্ত্রে আদর্শ ভাগে? (খ) একটি build মিথ্যা লাল হওয়ার সম্ভাবনা কত? (গ) প্রতিটি fail হওয়া test একবার আবার চালানো হলে আর দ্বিতীয়বার pass করলে pass ধরা হলে, মিথ্যা লাল কত? (ঘ) দল integration-এর flaky-সম্ভাবনা ০.০০০১-এ নামাল। মিথ্যা লাল ৫%-এর নিচে রাখতে সর্বোচ্চ কয়টি E2E রাখা যায়?

    (ক) ১২০০ × ০.০০৪ = ৪.৮ সেকেন্ড; ১৫০ × ০.৮ = ১২০; ২৫ × ২৪ = ৬০০. মোট ৭২৪.৮ সেকেন্ড ≈ ১২.১ মিনিট. ৪টি যন্ত্রে ৭২৪.৮ ÷ ৪ = ১৮১.২ সেকেন্ড ≈ ৩.০ মিনিট (সবচেয়ে লম্বা একক test ২৪ সেকেন্ড, তাই ভাগ সম্ভব)। আবারও E2E-ই সময়ের ৮৩%.

    (খ) সব সবুজের সম্ভাবনা = (১ − ০.০০০৫)১৫০ × (১ − ০.০০৪)২৫ ≈ ০.৯২৭৭ × ০.৯০৪৭ ≈ ০.৮৩৯৩. মিথ্যা লাল ≈ ১৬.১%. (অনুমান Σ n × p = ০.১৭৫ — একটু বেশি।) আলাদা করে: integration একাই ৭.২%, E2E একাই ৯.৫%.

    (গ) একটি test-এর দুবারই flaky হওয়ার সম্ভাবনা p২: integration-এ ০.০০০০০০২৫, E2E-তে ০.০০০০১৬. মিথ্যা লাল = ১ − (১ − ০.০০০০০০২৫)১৫০ × (১ − ০.০০০০১৬)২৫ ≈ ০.০৪৪%. প্রায় শূন্য — এবং ঠিক এই কারণেই retry বিপজ্জনক: flakiness নিজে কমেনি, শুধু চোখের আড়ালে গেছে (ভাবনার প্রশ্ন ০৪)।

    (ঘ) Integration-এর অংশ এখন ১ − (০.৯৯৯৯)১৫০ ≈ ১.৫%. n-টি E2E-সহ মিথ্যা লাল = ১ − ০.৯৮৫১ × ০.৯৯৬n. n = ৯ হলে ≈ ৪.৯৮% (৫%-এর ঠিক নিচে), n = ১০ হলে ৫%-এর ওপরে। তাই সর্বোচ্চ ৯টি E2E — বর্তমান ২৫টির মধ্যে ১৬টি নিচের স্তরে নামাতে, অথবা E2E-গুলোর flakiness কমাতে হবে। (সব সংখ্যা একটি ছোট Python script চালিয়ে মিলিয়েছি।) শিক্ষা: integration ঠিক না করলে (খ-তে তার একার ৭.২%), শুধু E2E কমিয়ে ৫%-এ পৌঁছানো যেত না।

  2. আউটপুট অনুমান করুন: অংশ ৬-এর contract_check.py-এ শেষ loop-এর আগে নিচের provider যোগ করে শুধু এটিকেই যাচাই করা হলো: for name, prov in [("v3", provider_v3)]:. প্রোগ্রাম ঠিক কী ছাপবে, আর কেন?
    def provider_v3(req):
        return 200, {"id": "7", "state": "PAID", "total": 450,
                     "total_v2": 450, "addr": "Mirpur 10"}

    আসলে চালালে আউটপুট:

    provider v3:
      mobile-app GET /orders/7: FAIL 'id' is not int
      courier    GET /orders/7: FAIL 'id' is not int; missing field 'address'
      can deploy? NO - a consumer would break

    id এখন "7" — লেখা (str), সংখ্যা নয়; দুই consumer-ই int চায়, তাই দুজনেরই প্রথম সমস্যা একই। Mobile app-এর state আর total ঠিক আছে — বাড়তি total_v2 কাউকে ভাঙায় না। Courier-এর address নেই, কারণ নাম বদলে addr — দ্বিতীয় সমস্যা। লক্ষ করুন, মান একই থাকলেও (“7”) শুধু type বদল একটি breaking change: JSON-এ 7 আর "7" আলাদা, আর একটি কড়া type-এর client (Kotlin, Swift, TypeScript-এর validation) এখানে ভেঙে পড়ে।

  3. Test-কৌশল নকশা করুন: একটি কাল্পনিক বাস-টিকিট অ্যাপ — ঈদের আগে ভিড়ে লাখো মানুষ একসাথে আসেন। অংশগুলো: mobile app আর website; search-api (রুট আর সময়সূচি); booking-service (আসন ধরে রাখা আর বুকিং, নিজের PostgreSQL); বাইরের payment gateway; SMS পাঠানোর বাইরের service. অংশ ৮-এর ঝুঁকিগুলো মাথায় রেখে প্রতিটি স্তরে কী test রাখবেন, কয়টি E2E, contract কোথায়, আর flakiness কীভাবে ঠেকাবেন — একটি পরিকল্পনা লিখুন।

    একটি আদর্শ উত্তর (একমাত্র নয়):

    • Static আর unit (সবচেয়ে বেশি, শত শত): ভাড়ার হিসাব (দূরত্ব, শ্রেণি, ছাড়), আসন ধরে রাখার সময়সীমা (ধরুন ১০ মিনিট — boundary: ৯:৫৯, ১০:০০, ১০:০১), বাতিলের নিয়ম। ঘড়ি inject করা, যাতে মাঝরাতের test নির্ভরযোগ্য।
    • Narrow integration (কয়েক ডজন): booking-service-এর repository আসল PostgreSQL-এ (container), rollback-সহ। সবচেয়ে জরুরি: একই আসন দুজন একসাথে ধরতে পারেন না — দুটি connection থেকে একসাথে চেষ্টা (এটি একটি transaction-এর rollback-এ হয় না, তাই আলাদা database আর পরে পরিষ্কার)। Payment client নকল HTTP server-এর বিরুদ্ধে: সফল, প্রত্যাখ্যাত, timeout, একই অনুরোধ দুবার (idempotent কি না)।
    • Contract: app ও website ↔ search-api আর booking-service — consumer-driven contract, provider-এর build-এ verification. Payment gateway আর SMS আমাদের নিয়ন্ত্রণে নেই: তাদের নথি থেকে নিজেদের দিকের চুক্তি, আর রাতে তাদের sandbox-এর বিরুদ্ধে একটি ছোট non-blocking suite.
    • E2E (৩–৫টি): খোঁজা → আসন → টাকা → নিশ্চিতকরণ; টাকা ব্যর্থ হলে আসন ছেড়ে দেওয়া; টিকিট বাতিল। Setup API দিয়ে, page object, বাইরের service-এর জায়গায় service virtualization, কোনো sleep নয়।
    • Flakiness-এর প্রতিরোধ: প্রতিটি test নিজের রুট-তারিখ-আসন (অনন্য id); ভাগ করা staging-এ E2E নয়; এলোমেলো ক্রমে চালানো; flaky-হারের ড্যাশবোর্ড আর মালিকসহ quarantine. ঈদের ভিড় functional test-এর বিষয় নয় — load test আলাদা, release-এর আগে।
    • CI-এর ক্রম: static + unit → narrow integration + contract verification → ৩–৫টি E2E → merge. লক্ষ্য: PR-এ ১০ মিনিটের নিচে।
  4. নির্ণয় আর সমালোচনা: নিচের test একজন ডেভেলপারের কম্পিউটারে (ঢাকা, Asia/Dhaka, UTC+6) সবসময় pass করে, কিন্তু CI-তে (UTC) প্রতিদিন রাত ১২টা থেকে সকাল ৬টা (ঢাকার সময়) পর্যন্ত fail করে, আর কখনো কখনো দিনের বেলাও পুরো suite-এ fail. কারণগুলো খুঁজুন আর ঠিক করার উপায় লিখুন।
    CART = []                                  # module-level list
    
    def test_order_is_dated_today():
        CART.append({"sku": "TEA-01", "qty": 2})
        order = place_order(CART)              # uses datetime.now() inside
        assert order.date == date.today()      # date.today() uses the local time zone
        assert len(order.items) == 1
    • কারণ ১ — সময়-অঞ্চল: ধরা যাক place_order ভেতরে ব্যবসার তারিখ ঠিক করে Asia/Dhaka ধরে (যেমন datetime.now(ZoneInfo("Asia/Dhaka"))), আর test তুলনা করে যন্ত্রের স্থানীয় date.today()-র সাথে — CI-তে সেটি UTC. ঢাকায় রাত ১২টা থেকে সকাল ৬টা পর্যন্ত ঢাকার তারিখ UTC-র তারিখের চেয়ে এক দিন এগিয়ে থাকে — দুই দিকের তারিখ মেলে না। ডেভেলপারের কম্পিউটারের সময়-অঞ্চল নিজেই Asia/Dhaka, তাই সেখানে দুই দিক সবসময় মেলে, আর ভুলটি কখনো ধরা পড়েনি। আর মাঝরাতের ঠিক আগে-পরে চালালে now() আর today()-র মাঝেই তারিখ বদলাতে পারে।
    • কারণ ২ — ভাগ করা অবস্থা: CART module-স্তরের list. অন্য কোনো test আগে এতে item যোগ করলে (বা এই test একই process-এ দুবার চললে) len(order.items) == 1 ভাঙে — order-dependent, তাই “কখনো কখনো দিনের বেলাও”।
    • সমাধান: (ক) ঘড়ি inject করা: place_order(cart, clock=fixed_clock("2026-03-15T23:30:00+06:00")) — test নিজে সময় ঠিক করে, আর মাঝরাতের পাশের boundary-ও ইচ্ছে করে test করা যায়। (খ) কোডে নিয়ম স্পষ্ট: ব্যবসার তারিখ কোন সময়-অঞ্চলে (বাংলাদেশের দোকানে সম্ভবত Asia/Dhaka), আর তারিখ সংরক্ষণ UTC-তে। (গ) CART-এর বদলে test-এর ভেতরে নতুন list, বা একটি fixture যা প্রতিবার নতুন দেয়। (ঘ) যাচাই: CI-তে TZ বদলে আর test-এর ক্রম এলোমেলো করে চালানো, যাতে এ ধরনের ভুল লুকিয়ে না থাকে।
    • সমালোচনা: test-টি দুটি আলাদা জিনিস যাচাই করছে (তারিখ আর item-এর সংখ্যা); দুটি test-এ ভাগ করলে ভাঙার কারণ নাম দেখেই বোঝা যায় (পর্ব ৪৬)।

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

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

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

  1. 🔴 CI লাল, আবার চালালে সবুজ — সন্দেহ
  2. 📊 একই commit-এ বারবার চালানোর পরিসংখ্যান দিয়ে flaky নিশ্চিত করা
  3. 🧪 স্থানীয়ভাবে লুপে চালিয়ে পুনরুৎপাদন
  4. 🚧 মালিক, ticket আর সময়সীমাসহ quarantine
  5. 🔍 মূল কারণ খুঁজে ঠিক করা (যেমন sleep-এর বদলে শর্তের অপেক্ষা)
  6. ✅ বহুবার সবুজ দেখে মূল suite-এ ফেরত

একটি দলের suite-এ ৮০টি unit test, ৪০টি integration test আর ৪০০টি UI-ভিত্তিক end-to-end test, সাথে release-এর আগে দুই দিনের হাতে করা test. এই অবস্থার প্রচলিত নাম কী, আর প্রধান সমস্যা কী?

  • Testing trophy — integration কম বলে সমস্যা
  • Ice-cream cone — ধীর, দামি, flaky suite আর দেরিতে feedback
  • Test honeycomb — microservice-এর জন্য আদর্শ
  • Test pyramid — এটিই সুপারিশকৃত আকার

একটি suite-এ ৫০টি test আছে, প্রতিটি স্বাধীনভাবে ১% সম্ভাবনায় flaky (কোডে ভুল ছাড়াই fail). একটি run-এ অন্তত একটি test মিথ্যা fail করার সম্ভাবনা মোটামুটি কত?

  • ১%
  • ৫০%
  • প্রায় ৪০%
  • প্রায় শূন্য, কারণ ০.০১৫০

Top-down integration testing-এ ওপরের module আগে test হয়, কিন্তু নিচের কয়েকটি module এখনো তৈরি হয়নি। তাদের জায়গায় কী ব্যবহার হয়?

  • Stub — অনুপস্থিত নিচের module-এর নকল, ঠিক করা উত্তর দেয়
  • Driver — ওপরের module-এর নকল
  • Page object
  • Production database-এর একটি কপি

একটি mobile app-এর unit test-এ order API-র একটি mock আছে, যা {"total": 450} ফেরত দেয়। API-র দল total-এর নাম বদলে amount করল; app আর API দুই দিকের test-ই সবুজ, কিন্তু production-এ app-এ দাম ফাঁকা। কোন ব্যবস্থা এটি deploy-এর আগেই সবচেয়ে নির্ভরযোগ্যভাবে ধরত?

  • App-এ আরও বেশি mock-ভিত্তিক unit test
  • App-এর test coverage ১০০% করা
  • Release-এর পরে QA দলের হাতে দেখা
  • Consumer-driven contract test, যা provider-এর build-এ যাচাই হয়

দুটি database test একা চালালে দুটিই pass করে, কিন্তু একটি নির্দিষ্ট ক্রমে পুরো suite চালালে দ্বিতীয়টি fail করে। প্রথম test একটি অর্ডার INSERT করে শেষে COMMIT করে। সবচেয়ে ভালো সমাধান কোনটি?

  • Test-গুলো সবসময় একই নির্দিষ্ট ক্রমে চালানো
  • প্রতিটি test নিজের transaction-এ চালিয়ে শেষে ROLLBACK, যাতে কোনো data রেখে না যায়
  • দ্বিতীয় test-কে তিনবার retry করা
  • দ্বিতীয় test মুছে ফেলা

একটি E2E test-এ লেখা: “Pay বোতামে ক্লিক, তারপর ৩ সেকেন্ড অপেক্ষা, তারপর নিশ্চিতকরণের লেখা আছে কি না দেখা।” CI-তে এটি মাঝে মাঝে fail করে। সবচেয়ে ভালো পরিবর্তন কোনটি?

  • অপেক্ষা ৩ থেকে ১০ সেকেন্ড করা
  • Test-টি মুছে ফেলা, কারণ E2E সবসময় flaky
  • নির্দিষ্ট sleep-এর বদলে নিশ্চিতকরণের লেখা দেখা না যাওয়া পর্যন্ত অপেক্ষা (একটি deadline-সহ)
  • CI-তে test-টি নিজে থেকে পাঁচবার retry করা

Martin Fowler-এর ভাগ অনুযায়ী একটি “narrow integration test”-এর সবচেয়ে ভালো উদাহরণ কোনটি?

  • একটি repository class-কে container-এ চালানো আসল PostgreSQL-এর সাথে test করা, বাকি system ছাড়া
  • Order, payment আর inventory — সব service চালু করে একটি অর্ডারের পুরো পথ test করা
  • Browser খুলে login থেকে checkout পর্যন্ত ক্লিক করা
  • সব dependency mock করে একটি ছাড়ের function-এর হিসাব যাচাই করা

শব্দকোষ

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

মনে রাখুন

  1. Pyramid: নিচে অনেক unit, মাঝে কম integration/service, ওপরে অল্প E2E/UI (Cohn, ২০০৯). প্রতিটি ঝুঁকি সবচেয়ে নিচের যে স্তরে ধরা যায়, সেখানে। Trophy আর honeycomb ঝুঁকি যেখানে, সেখানে ভার দেয়; ice-cream cone এড়ান।
  2. হিসাব: সময় = Σ n × t; মিথ্যা লাল = 1 − Π(1 − p)n ≈ Σ n × p. অল্প কয়েকটি ধীর, flaky E2E-ই প্রায়ই সময় আর মিথ্যা লালের বড় অংশ।
  3. Integration: big-bang-এ ভুল খোঁজা কঠিন; top-down (stub), bottom-up (driver), sandwich — একবারে একটি নতুন জোড়া। Narrow বেশি, broad কম; আসল database container-এ, প্রতিটি test-এ rollback, নিজের data.
  4. Contract test: consumer লেখে কী পাঠায় আর কোন field পড়ে; provider নিজের build-এ verification করে। Mock drift আর breaking change deploy-এর আগে ধরা পড়ে; বদল হয় expand, তারপর contract.
  5. Flaky test: কারণ অপেক্ষা, ভাগ করা অবস্থা, ক্রম, বাইরের service, সময়-অঞ্চল, এলোমেলো মান, concurrency. পরিসংখ্যান → পুনরুৎপাদন → মালিকসহ quarantine → মূল কারণ ঠিক → ফেরত। অন্ধ retry নয়, আর suite রাখুন দ্রুত ও hermetic.

পরের পর্বে: Testing মডিউল এখানে শেষ। পর্ব ৫২ থেকে উন্নত Git আর automation-এর মডিউল শুরু — প্রথমে Git-এর ভেতরে: blob, tree, commit আর content-addressed storage. যে commit-এর hash এতদিন দেখেছেন, সেটি আসলে কী থেকে তৈরি, নিজে হাতে বানিয়ে দেখবেন।

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

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