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

Mock, MSW আর test data — flaky test কমানো

Mocking, Mock Service Worker and Test Data — Taming Flaky Tests

Taught in Bangla, this lesson decides what to mock and what to keep real, shows how over-mocking gives green tests and broken production, uses Mock Service Worker at the network boundary across Vitest, the browser and Storybook, simulates errors and slow responses, builds test data with factories, builders and object mothers, seeds random data including Bangla names, reproduces seven causes of flaky tests by hand, and finds and quarantines them.

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

  • কোনটি mock করবেন আর কোনটি আসল রাখবেন, আর over-mocking কীভাবে সবুজ test-এর আড়ালে production ভাঙে
  • MSW 3 দিয়ে handler লেখা, error আর ধীর উত্তর বানানো, অচেনা অনুরোধে test থামানো, আর একই handler browser-এ চালানো
  • Factory, builder আর object mother দিয়ে test data, seed দেওয়া faker, বাংলা নাম আর ঠিকানা
  • Flaky test-এর সাতটি কারণ নিজের হাতে ঘটানো, --repeats দিয়ে খুঁজে বের করা আর সারানো
  • নকল ঘড়ি (vi.useFakeTimers), quarantine, retry-এর বিপদ আর contract test দিয়ে mock-কে সৎ রাখা

১ · কী নকল করবেন, কী আসল রাখবেন

মডিউল ১১-এর আগের পর্বগুলোতে “বাজারঘর”-এর জন্য নানা স্তরের test লেখা হয়েছে: unit test (পর্ব ৭৩), component test (পর্ব ৭৪), test database-সহ API test (পর্ব ৭৫) আর Playwright দিয়ে পুরো পথ (পর্ব ৭৬)। প্রতিটিতে একটি প্রশ্ন বারবার এসেছে: এই test-এ কোন অংশ আসল, আর কোন অংশ নকল? এই পর্ব সেই প্রশ্নের উত্তর গুছিয়ে দেয়, তারপর test-এর সবচেয়ে বিরক্তিকর সমস্যায় যায়: flaky test, অর্থাৎ যে test কোড না বদলালেও কখনো পাস করে, কখনো ফেল।

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

সফটওয়্যারে চুলা আর হাঁড়ি হলো আপনার নিজের কোড, নিজের database আর নিজের schema. আড়ত হলো বাইরের সেবা: payment gateway, SMS gateway, email পাঠানোর সেবা, কুরিয়ারের API. এদের বদলে যা বসান, তার সাধারণ নাম test double. Software Engineering কোর্সের test double পর্বে dummy, stub, spy, mock আর fake-এর পার্থক্য বিস্তারিত আছে; এখানে আমরা দৈনন্দিন কথার মতো “mock” বলব, কিন্তু কোনটি আসলে কী করছে তা খেয়াল রাখব।

Test-এর সীমানা: ভেতরে আসল, network-এর বাইরে নকল

Keep your own code and database real; mock what you do not own at the network edge

কী আসল, কী নকল আসল রাখুন React component ProductList, Cart Route আর service Express, নিয়মের কোড Repository Prisma, SQL Test database PostgreSQL, পর্ব ৭৫ schema, constraint, SQL সব সত্যিই চলে MSW নকল করুন Payment SMS Email কুরিয়ার API আপনার নয়, ধীর, খরচের বা অনিশ্চিত Repository বা DB mock = SQL কখনো চলে না ধীর হলে আসল DB দ্রুত করুন, নকল নয়
ছবি ৭৭.১ — বেগুনি ড্যাশ দেওয়া রেখাটি network-এর সীমানা। বাম দিকের সবকিছু আপনার, তাই আসল চলে; ডান দিকের সেবা আপনার নয়, তাই নকল। MSW ঠিক সীমানায় বসে অনুরোধ ধরে, আপনার কোডের ভেতরে কিছু বদলায় না।

সিদ্ধান্তের জন্য চারটি প্রশ্ন যথেষ্ট। নিচের টেবিলে বাজারঘরের সাধারণ নির্ভরতাগুলো সেই প্রশ্ন ধরে সাজানো।

নির্ভরতাআপনার?ধীর বা খরচের?সিদ্ধান্ত
Payment gateway (পর্ব ৬৫)নাহ্যাঁ, আসল টাকা আর keyনকল: MSW handler বা sandbox
SMS আর email (পর্ব ৬৩)নাহ্যাঁ, প্রতিটি বার্তার খরচ, আসল মানুষের ফোননকল, আর পাঠানো body যাচাই
নিজের PostgreSQL আর Prismaহ্যাঁকিছুটা, কিন্তু মাপা যায়আসল test database (পর্ব ৭৫)
নিজের repository আর serviceহ্যাঁনাআসল; unit test-এ শুধু খাঁটি নিয়মের ফাংশন আলাদা
ঘড়ি (Date.now, timer)না, OS-এরঅনিশ্চিতনকল ঘড়ি: vi.useFakeTimers
এলোমেলো সংখ্যানাঅনিশ্চিতSeed দেওয়া generator
মূল ধারণা

Mock করার তিনটি ভালো কারণ: জিনিসটি আপনার নয়, ধীর বা খরচের, অথবা অনিশ্চিত (ঘড়ি, এলোমেলো সংখ্যা, বাইরের network). কোনোটি না খাটলে আসল জিনিস ব্যবহার করুন। “Test দ্রুত হবে” একা যথেষ্ট কারণ নয়, যদি তাতে test আসল প্রশ্নের উত্তর দেওয়া বন্ধ করে।

২ · Over-mocking — সবুজ test, ভাঙা production

Over-mocking মানে এত বেশি নকল করা যে test আর আসল কোডের আচরণ যাচাই করে না, শুধু যাচাই করে “আমি যা ভেবেছিলাম, কোড তা-ই ডেকেছে”। সবচেয়ে সাধারণ রূপ: নিজের repository বা ORM client mock করা। নিচের ঘটনাটি ল্যাবে সত্যিই ঘটানো হয়েছে।

বাজারঘরে একটি migration এসেছে: orders টেবিলের column total_paisa-এর নাম বদলে amount_paisa. Migration লেখা হলো, কিন্তু repository-র SQL বদলানো হলো না। Service-এর test repository-কে vi.fn() দিয়ে নকল করে:

test('নিজের repository mock করে: সবুজ', () => {
  const repo = { insert: vi.fn() };
  expect(placeOrder(repo, 'BG-9001', 1, 84000).status).toBe('PLACED');
  expect(repo.insert).toHaveBeenCalledWith({ ref: 'BG-9001', customerId: 1, totalPaisa: 84000 });
});

test('আসল (in-memory) database-এ: লাল', () => {
  const db = new DatabaseSync(':memory:');
  db.exec('CREATE TABLE orders (ref TEXT PRIMARY KEY, customer_id INTEGER, amount_paisa INTEGER NOT NULL)');
  expect(placeOrder(makeOrderRepo(db), 'BG-9001', 1, 84000).status).toBe('PLACED');
});

এখানে দ্বিতীয় test-এ Node-এর built-in node:sqlite ব্যবহার করা হয়েছে, শুধু দেখানোর জন্য যে ছোট একটি আসল database-ও ভুলটি সাথে সাথে ধরে। বাজারঘরের আসল কাজে এটি হবে পর্ব ৭৫-এর PostgreSQL test database. সত্যিকারের run, Node 24.15.0 আর Vitest 5.1.0:

$ npx vitest run tests/overmock.test.ts --reporter=verbose
 ✓ tests/overmock.test.ts > নিজের repository mock করে: সবুজ 2ms
 × tests/overmock.test.ts > আসল (in-memory) database-এ: লাল 10ms
   → table orders has no column named total_paisa

 FAIL  tests/overmock.test.ts > আসল (in-memory) database-এ: লাল
Error: table orders has no column named total_paisa
 ❯ Object.insert src/orders.ts:7:10
 ❯ placeOrder src/orders.ts:16:8
 ❯ tests/overmock.test.ts:14:10
Serialized Error: { code: 'ERR_SQLITE_ERROR', errcode: 1, errstr: 'SQL logic error' }

 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)

প্রথম test একা থাকলে CI সবুজ, deploy হতো, আর প্রথম order-এই server 500 দিত। Mock-টি সেই প্রশ্নই করেনি, যার উত্তর জরুরি: SQL কি schema-র সাথে মেলে? একইভাবে mock-করা database duplicate-এর unique constraint (পর্ব ৪৩), N+1 query (পর্ব ৪২) বা rollback কিছুই দেখে না।

Over-mocking-এর লক্ষণ: (১) toHaveBeenCalledWith বেশি, ফলাফলের expect কম। (২) Refactor করলে test ভাঙে, অথচ আচরণ একই। (৩) একটি test-এ পাঁচ-ছয়টি vi.mock. (৪) নিজের লেখা module mock করা, যার কাজ শুধু database ডাকা। জিজ্ঞেস করুন: নকলটি সরালে test কি সত্যিই ধীর বা অনিশ্চিত হয়?

তাহলে vi.fn() কখন ঠিক? যখন সত্যিই “ডাকা হলো কি না” জানতে চান, আর ডাকা জিনিসটি সীমানার বাইরে, যেমন dependency injection দিয়ে আসা SMS client (পর্ব ২৯)। তবে HTTP স্তরে MSW দিয়ে নকল করা প্রায়ই ভালো, কারণ তাতে আপনার আসল HTTP client, header আর JSON তৈরিও test-এ চলে।

৩ · MSW — network-এর সীমানায় নকল

Mock Service Worker (MSW) একটি open-source library, যা Artem Zakharchenko-র হাতে শুরু হয় ২০১০-এর দশকের শেষ দিকে। এর মূল ভাবনা: আপনার কোড যেভাবে আছে সেভাবেই fetch ডাকুক; অনুরোধটি বাইরে যাওয়ার ঠিক আগে MSW ধরে ফেলবে, আর আপনার লেখা handler উত্তর দেবে। Browser-এ এটি একটি service worker দিয়ে কাজ করে, নামও সেখান থেকে। Node-এ (Vitest বা Jest-এ) এটি Node-এর HTTP আর fetch-এর স্তরে অনুরোধ ধরে; সেখানে কোনো service worker নেই।

লেখার সময়ের version: npm-এ msw-এর latest 3.0.3, অর্থাৎ নতুন বড় version 3. ল্যাবের বাকি: Node 24.15.0, Vitest 5.1.0, @testing-library/react 16.3.3, jsdom 30.1.3, React 19.3.1, Vite 8.3.5, @faker-js/faker 10.6.0, আর Edge-এ Playwright 1.64.1. বেশিরভাগ tutorial আর AI এখনো MSW 2-এর কোড দেখায়, তাই mswjs.io-র migration guide মিলিয়ে নেবেন।

ল্যাবের গঠন খুব ছোট। Handler এক জায়গায়, তারপর দুটি পাতলা ফাইল: একটি Node-এর জন্য, একটি browser-এর জন্য।

import { http, HttpResponse } from 'msw/http';

// একই handler: Vitest, browser-এর dev আর Storybook — তিন জায়গায়
export const handlers = [
  http.get('*/api/products', () =>
    HttpResponse.json([
      { id: 1, name: 'মিনিকেট চাল ৫ কেজি', pricePaisa: 42000 },
      { id: 2, name: 'মসুর ডাল ১ কেজি', pricePaisa: 13490 },
    ]),
  ),
  http.post('https://sms.example.test/v1/send', async ({ request }) => {
    const body = (await request.json()) as { to: string };
    return HttpResponse.json({ id: 'sms_test_1', to: body.to, status: 'queued' }, { status: 202 });
  }),
];
// src/mocks/node.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

// vitest.setup.ts (vitest.config.ts-এ setupFiles দিয়ে যুক্ত)
import '@testing-library/jest-dom/vitest';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { cleanup } from '@testing-library/react';
import { server } from './src/mocks/node';

beforeAll(() => server.listen({ onUnhandledFrame: 'error' }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());

তিনটি জিনিস লক্ষ করুন। */api/products-এর তারকা চিহ্ন যেকোনো origin মেলায়, তাই jsdom বা dev server, একই handler চলে। server.resetHandlers() প্রতিটি test-এর পরে সেই test-এর যোগ করা handler সরায়। আর onUnhandledFrame: 'error': handler না থাকলে test ব্যর্থ হয়, আসল network-এ যায় না।

শেষেরটি পরীক্ষা করা হলো। কেউ coupon API-র handler লিখতে ভুলে গেছেন, আর test সেই API ডাকে:

$ npx vitest run tests/unhandled.test.ts --reporter=verbose
stderr | tests/unhandled.test.ts > coupon যাচাই
[MSW] Error: intercepted a request without a matching request handler:

  • GET https://api.bazarghar.test/api/coupons/EID10

If you still wish to intercept this unhandled request, please create a request handler for it.

 × tests/unhandled.test.ts > coupon যাচাই 24ms
   → fetch failed
TypeError: fetch failed
Caused by: InternalError: [MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledFrame" option.

এবার একই test, কিন্তু setup ফাইলে MSW 2-এর পুরোনো নাম onUnhandledRequest: 'error', যা AI আর পুরোনো blog প্রায়ই লেখে। সত্যিকারের run:

stderr | tests/unhandled.test.ts > coupon যাচাই
[MSW] Warning: intercepted a request without a matching request handler:
  • GET https://api.bazarghar.test/api/coupons/EID10
 × coupon যাচাই 35ms
TypeError: fetch failed
Caused by: Error: getaddrinfo ENOTFOUND api.bazarghar.test

“Error” হয়ে গেছে “Warning”, আর অনুরোধটি সত্যিই network-এ বেরিয়ে গেছে: DNS-এ ঠিকানাটি খোঁজা হয়েছে (ENOTFOUND). MSW 3 পুরোনো option-টি নীরবে উপেক্ষা করে ডিফল্ট “warn”-এ ফিরেছে; ঠিকানাটি আসল সেবার হলে test সেখানে অনুরোধ পাঠাত। TypeScript এটি ধরে: tsc-তে error TS2353: … 'onUnhandledRequest' does not exist in type …. তাই test-এর setup ফাইলও type-check-এর আওতায় রাখুন।

বিষয়MSW 2MSW 3
http, HttpResponsefrom 'msw'from 'msw/http'
delayfrom 'msw'from 'msw/utils/delay'
অচেনা অনুরোধonUnhandledRequestonUnhandledFrame (WebSocket-ও ধরে)
setupServer, setupWorkermsw/node, msw/browserএকই জায়গায়
worker.stop()সাথে সাথেPromise, তাই await
Node.jsপুরোনো version-ওmigration guide অনুযায়ী 22 বা তার বেশি

একটি অনুরোধ MSW-এর ভেতরে কোন পথে যায়, ধাপে ধাপে দেখুন। উদাহরণ পর্ব ৭৪-এর ধাঁচের একটি component test: ProductList mount হয়ে fetch করে।

  1. ComponentfetchuseEffect
  2. Interceptormsw/nodesocket খোলে না
  3. Handler খোঁজাuse → baseপ্রথম মিল
  4. Resolver200HttpResponse
  5. TestfindByTextDOM দেখে
Componentপ্রথম render-এর পরে
DOM: <p>লোড হচ্ছে…</p>
useEffect → fetch(
  new URL('/api/products', location.origin))
MSW interceptorserver.listen() চালু
ধরা হলো: GET …/api/products
DNS নেই, TCP নেই, আসল server নেই
এখন handler-এর তালিকায় খোঁজা
তালিকাউপর থেকে নিচে
১. server.use(...) দিয়ে যোগ করা  → এই test-এ নেই
২. handlers[0] GET */api/products → মিলল
   (না মিললে → onUnhandledFrame: 'error')
Resolver-এর উত্তরআসল Response object
200 OK  content-type: application/json
[{"id":1,"name":"মিনিকেট চাল ৫ কেজি","pricePaisa":42000},
 {"id":2,"name":"মসুর ডাল ১ কেজি","pricePaisa":13490}]
Component আর testসত্যিকারের run
setItems, setState('ok') → re-render
<li>মিনিকেট চাল ৫ কেজি — ৳420.00</li>
<li>মসুর ডাল ১ কেজি — ৳134.90</li>
✓ সফল: দুটি পণ্য দেখায় 122ms
  1. Component অনুরোধ পাঠায়। প্রথম render-এ “লোড হচ্ছে…”, তারপর useEffect সাধারণ fetch ডাকে। Component জানেই না যে test চলছে; কোডে কোনো if (test) নেই।
  2. Interceptor ধরে ফেলে। server.listen() চালু থাকায় MSW Node-এর fetch-এর স্তরে অনুরোধটি থামায়। কোনো DNS খোঁজা বা TCP সংযোগ হয় না।
  3. Handler খোঁজা। আগে সেই test-এর server.use দিয়ে যোগ করা handler, তারপর মূল তালিকা। প্রথম যেটি method আর URL মেলায়, সেটিই উত্তর দেবে। কিছু না মিললে 'error' নীতিতে test ব্যর্থ।
  4. Resolver উত্তর বানায়। HttpResponse.json একটি সত্যিকারের Web Response বানায়: status 200, JSON header আর body. Component-এর কাছে এটি আসল server-এর উত্তরের মতোই।
  5. Test DOM দেখে। Component state বদলে দুটি li দেখায়। Test শুধু ব্যবহারকারী যা দেখেন তা-ই যাচাই করে: findByText(/মিনিকেট চাল/). ল্যাবের run-এ test-টি ১২২ মিলিসেকেন্ডে পাস।

MSW-এর ভেতরের কাজ বোঝার জন্য নিচে একটি ছোট শিক্ষার মডেল। এটি আসল MSW নয়; শুধু ধারণাটি দেখায়: handler-এর একটি টেবিল, use দিয়ে সাময়িক handler সামনে বসানো, resetHandlers দিয়ে সরানো, আর একটি নকল ঘড়ি, যাতে “৫ সেকেন্ড দেরি” পরীক্ষা করতে সত্যিই ৫ সেকেন্ড অপেক্ষা করতে না হয়।

// শিক্ষার মডেল: MSW-এর মতো handler-এর টেবিল আর নকল ঘড়ি (আসল MSW নয়)
const base = [
  { method: "GET", path: "/api/products", reply: () => ({ status: 200, body: "[চাল, ডাল]" }) },
];
let overrides = [];
const use = (h) => overrides.unshift(h);          // server.use-এর মতো: আগে খোঁজা হয়
const resetHandlers = () => { overrides = []; };

let clock = 0;                                     // নকল ঘড়ি, মিলিসেকেন্ডে
function fakeFetch(method, path, timeoutMs = 1000) {
  const h = [...overrides, ...base].find((x) => x.method === method && x.path === path);
  if (!h) return `ERROR unhandled ${method} ${path} (onUnhandledFrame: "error")`;
  const r = h.reply();
  const waited = r.delayMs ?? 0;
  if (waited > timeoutMs) { clock += timeoutMs; return `TIMEOUT after ${timeoutMs}ms`; }
  clock += waited;
  return r.networkError ? "TypeError: Failed to fetch" : `${r.status} ${r.body ?? ""} (+${waited}ms)`;
}

console.log("১ সফল  :", fakeFetch("GET", "/api/products"));
use({ method: "GET", path: "/api/products", reply: () => ({ status: 500, body: "" }) });
console.log("২ error:", fakeFetch("GET", "/api/products"));
resetHandlers();
use({ method: "GET", path: "/api/products", reply: () => ({ status: 200, body: "[চাল]", delayMs: 300 }) });
console.log("৩ ধীর  :", fakeFetch("GET", "/api/products"));
use({ method: "GET", path: "/api/products", reply: () => ({ delayMs: 5000 }) });
console.log("৪ আটকে :", fakeFetch("GET", "/api/products"));
use({ method: "GET", path: "/api/products", reply: () => ({ networkError: true }) });
console.log("৫ নেট  :", fakeFetch("GET", "/api/products"));
resetHandlers();
console.log("৬ ভুলে :", fakeFetch("GET", "/api/coupons/EID10"));
console.log("নকল ঘড়ি মোট এগোল:", clock, "ms; আসল সময় প্রায় শূন্য");

প্রথম লাইন 200 [চাল, ডাল] (+0ms): মূল handler. তারপর use দিয়ে সামনে বসানো handler জিতেছে: 500. resetHandlers-এর পরে ধীর উত্তর (+300ms), যা component-এর “লোড হচ্ছে” অবস্থা দেখার জন্য দরকার। চতুর্থটি TIMEOUT after 1000ms: উত্তর ৫ সেকেন্ডে আসত, কিন্তু client ১ সেকেন্ড পরে হাল ছেড়েছে। পঞ্চমটি network-ই ভেঙে যাওয়া, যা MSW-এ HttpResponse.error(). ষষ্ঠটি সবচেয়ে গুরুত্বপূর্ণ: কোনো handler নেই, তাই ERROR unhandled; আসল network-এ যাওয়ার প্রশ্নই নেই। শেষ লাইনে নকল ঘড়ি ১৩০০ ms এগিয়েছে (৩০০ + ১০০০), অথচ block চলেছে প্রায় সাথে সাথে।

৪ · Error, ধীর উত্তর আর একই handler তিন জায়গায়

বেশিরভাগ বাগ লুকিয়ে থাকে সুখী পথের বাইরে: server 500 দিল, উত্তর আসতে দেরি হলো, বা network-ই নেই। আসল server-কে এমন আচরণ করানো কঠিন; MSW-এ এটি এক লাইন। নিচের component test-গুলো পর্ব ৭৪-এর Testing Library-র ধাঁচে লেখা, প্রতিটি test শুধু নিজের প্রয়োজনের handler server.use দিয়ে সামনে বসায়:

import { http, HttpResponse } from 'msw/http';
import { delay } from 'msw/utils/delay';

test('server 500 দিলে বার্তা দেখায়', async () => {
  server.use(http.get('*/api/products', () => new HttpResponse(null, { status: 500 })));
  render(<ProductList />);
  expect(await screen.findByRole('alert')).toHaveTextContent('পণ্য আনা যায়নি');
});

test('ধীর উত্তর: আগে লোড, পরে তালিকা', async () => {
  server.use(http.get('*/api/products', async () => {
    await delay(300);
    return HttpResponse.json([{ id: 9, name: 'চিনি ১ কেজি', pricePaisa: 13000 }]);
  }));
  render(<ProductList />);
  expect(screen.getByText('লোড হচ্ছে…')).toBeInTheDocument();
  expect(await screen.findByText(/চিনি/)).toBeInTheDocument();
});

test('network ভেঙে গেলে', async () => {
  server.use(http.get('*/api/products', () => HttpResponse.error()));
  render(<ProductList />);
  expect(await screen.findByRole('alert')).toBeInTheDocument();
});
$ npx vitest run tests/product-list.test.tsx --reporter=verbose
 RUN  v5.1.0 ~/bazarghar
 ✓ tests/product-list.test.tsx > সফল: দুটি পণ্য দেখায় 122ms
 ✓ tests/product-list.test.tsx > server 500 দিলে বার্তা দেখায় 14ms
 ✓ tests/product-list.test.tsx > ধীর উত্তর: আগে লোড, পরে তালিকা 321ms
 ✓ tests/product-list.test.tsx > network ভেঙে গেলে 15ms
 Test Files  1 passed (1)
      Tests  4 passed (4)
   Duration  1.30s (environment 45%, tests 40%, setup 13%, transform 1%, import 1%)

ধীর test-টি ৩২১ ms নিয়েছে, কারণ delay(300) আসল সময় অপেক্ষা করে। “৩০ সেকেন্ড পরে timeout” পরীক্ষায় আসল ৩০ সেকেন্ড অপেক্ষা চলবে না; তখন নকল ঘড়ি (অংশ ৮)। সংখ্যা ছাড়া delay() docs অনুযায়ী browser-এ ১০০ থেকে ৪০০ ms-এর একটি এলোমেলো “বাস্তবসম্মত” দেরি দেয়, আর Node-এ সেই দেরি প্রায় শূন্য করে দেওয়া হয়; তাই test-এ নির্দিষ্ট সংখ্যাই নিরাপদ।

একই handler browser-এ। Backend তৈরি হওয়ার আগেই frontend-এর কাজ শুরু করতে বা একটি error-এর পাতা হাতে দেখতে একই handlers.ts browser-এ চালানো যায়। প্রথমে npx msw init public --save worker script (mockServiceWorker.js) public folder-এ রাখে। তারপর app চালুর আগে worker শুরু:

import { createRoot } from 'react-dom/client';
import { ProductList } from './ProductList';

async function enableMocking() {
  if (!import.meta.env.DEV || import.meta.env.VITE_MOCKS !== 'on') return;
  const { worker } = await import('./mocks/browser');   // setupWorker(...handlers)
  await worker.start({ onUnhandledFrame: 'bypass' });   // dev-এ বাকি অনুরোধ আসল পথে
}
enableMocking().then(() => createRoot(document.getElementById('root')!).render(<ProductList />));

Vite dev server 127.0.0.1:47781-এ চালিয়ে Edge-এ Playwright দিয়ে পাতাটি খোলা হলো; browser-এর console আর পাতার লেখা, সত্যিকারের run (একটি ফাঁকা favicon-এর 404 আর React DevTools-এর বার্তা বাদ):

[console] [MSW] Mocking enabled.
[console] Worker script URL: http://127.0.0.1:47781/mockServiceWorker.js
[request] GET /api/products
[console] [MSW] 01:55:29 GET /api/products (200 OK)
[page] মিনিকেট চাল ৫ কেজি — ৳420.00 | মসুর ডাল ১ কেজি — ৳134.90

অনুরোধটি সত্যিই browser থেকে বেরিয়েছে, service worker মাঝপথে ধরে উত্তর দিয়েছে। দুটি সতর্কতা। (১) import.meta.env.DEV দিয়ে পাহারা, যাতে production build-এ worker না যায়। (২) Dev-এ 'bypass', যাতে font, ছবি বা আসল API স্বাভাবিক পথে যায়; test-এ নীতি সবসময় 'error'. MSW 3-এ msw/vite নামে একটি Vite plugin-ও আছে; এই ল্যাবে চিরাচরিত setupWorker পথটি চালানো হয়েছে।

Storybook-এ। Storybook-এ component আলাদা করে দেখার সময়ও একই handler কাজে লাগে, সম্প্রদায়ের msw-storybook-addon-এর মাধ্যমে (লেখার সময় npm-এ 3.0.3), যেমন “error অবস্থা” নামের story-তে 500. এই ল্যাবে Storybook চালানো হয়নি; addon আর MSW-এর version মিলিয়ে নেবেন।

Handler-এর ডেটা: হাতে লেখা JSON সহজে পুরোনো হয়। উত্তর বানান অংশ ৫-এর factory দিয়ে, আর আকার মিলিয়ে নিন Zod schema (পর্ব ২৭) বা OpenAPI চুক্তির (পর্ব ৩৫) সাথে; অংশ ৯-এ এর পূর্ণ রূপ।

৫ · Test data — factory, builder আর object mother

প্রতিটি test-এর কিছু ডেটা লাগে: একজন গ্রাহক, একটি order, কয়েকটি item. হাতে লেখা বড় object প্রতিটি test-এ কপি করলে দুটি সমস্যা হয়। (১) কোন ঘরটি এই test-এর জন্য জরুরি, পড়ে বোঝা যায় না। (২) Model-এ একটি নতুন বাধ্যতামূলক field এলে পঞ্চাশটি ফাইল বদলাতে হয়। সমাধানের তিনটি পরিচিত ধরন আছে।

Factory

একটি ফাংশন, যা যুক্তিসংগত ডিফল্ট দিয়ে পূর্ণ object বানায়, আর over দিয়ে যেকোনো ঘর বদলাতে দেয়: buildOrder({ status: 'PAID' }). JavaScript-এ সবচেয়ে প্রচলিত।

Builder

ধাপে ধাপে বানানো: anOrder().paid().withItem(rice, 2).build(). পড়তে বাক্যের মতো; Java আর C#-এর জগতে বেশি দেখা যায়।

Object mother

নাম দেওয়া তৈরি চরিত্র: mother.vipCustomerInDhaka(), mother.refundedOrder(). সুবিধা নাম; বিপদ হলো চরিত্রের সংখ্যা দ্রুত বেড়ে যাওয়া।

বাস্তবে তিনটি মিশিয়ে চলে: নিচে factory, তার ওপর কয়েকটি নাম দেওয়া ছোট object mother ফাংশন, যারা factory-ই ডাকে। নিচের block-এ বাজারঘরের factory-র একটি ছোট রূপ। খেয়াল করুন, totalPaisa কখনো হাতে দেওয়া হয় না, item থেকে হিসাব হয়; নইলে test-এ এমন order তৈরি হতে পারে, যা বাস্তবে কখনো ঘটে না।

// Factory + override: test শুধু সেটুকু বলে, যা তার কাছে জরুরি
let nextId = 0;
const reset = () => { nextId = 0; };

function buildItem(over = {}) {
  nextId += 1;
  return { sku: `SKU-${nextId}`, qty: 1, pricePaisa: 10000, ...over };
}
function buildCustomer(over = {}) {
  nextId += 1;
  return { id: nextId, name: "নমুনা গ্রাহক", city: "ঢাকা", vip: false, ...over };
}
function buildOrder(over = {}) {
  const customer = over.customer ?? buildCustomer();
  const items = over.items ?? [buildItem()];
  const totalPaisa = items.reduce((s, i) => s + i.qty * i.pricePaisa, 0);
  return { ref: `BG-${9000 + customer.id}`, status: "PENDING", customer, items, totalPaisa,
    ...over.fields };
}

reset();
const plain = buildOrder();
console.log("ডিফল্ট:", plain.ref, plain.items.length, "টি item, মোট", plain.totalPaisa);

reset();
const big = buildOrder({
  customer: buildCustomer({ vip: true }),
  items: [buildItem({ qty: 2, pricePaisa: 42000 }), buildItem({ pricePaisa: 13490 })],
});
console.log("override:", big.ref, "vip =", big.customer.vip, "মোট", big.totalPaisa);
console.log("items:", big.items.map((i) => `${i.sku}x${i.qty}`).join(", "));

reset();
const paid = buildOrder({ fields: { status: "PAID" } });
console.log("শুধু অবস্থা বদল:", paid.ref, paid.status, paid.totalPaisa);

প্রথম লাইন ডিফল্ট: BG-9001 1 টি item, মোট 10000: কিছু না বলে order চাইলে একটি গ্রাহক আর ৳১০০-এর একটি item. দ্বিতীয় লাইনে শুধু দুটি কথা বলা হয়েছে, “VIP গ্রাহক” আর “এই দুটি item”, আর মোট 97490 নিজে থেকে হিসাব হয়েছে: 2 × 42000 + 1 × 13490. তৃতীয় লাইনে item-এর sku SKU-2x2, SKU-3x1: id-র ক্রম থেকে বোঝা যায় গ্রাহক আগে (id 1), তারপর দুটি item বানানো হয়েছে। শেষ লাইনে শুধু অবস্থা বদলানো হয়েছে, বাকি সব ডিফল্ট। প্রতিটি অংশের আগে reset() ডাকা হয়েছে, তাই প্রতিবার ref একই BG-9001; reset না থাকলে id বাড়তে থাকত, আর test-এর ক্রম বদলালে ref-ও বদলাত।

দ্বিতীয় অংশের factory-কল ধাপে ধাপে, কোন মুহূর্তে কোন id আর কোন মোট তৈরি হয়:

1reset();2const big = buildOrder({3  customer: buildCustomer({ vip: true }),4  items: [5    buildItem({ qty: 2, pricePaisa: 42000 }),6    buildItem({ pricePaisa: 13490 }),7  ],8});
Factory-র অবস্থা
nextId
customer
items
totalPaisa
ref, status
  1. শুরু পরিষ্কার। reset() id-র গণনা শূন্যে আনে। প্রতিটি test-এর আগে এটি চললে test-এর ক্রম বদলালেও id একই থাকে।
  2. আগে গ্রাহক। JavaScript object-এর ঘর ওপর থেকে নিচে মূল্যায়ন করে, তাই buildCustomer প্রথমে চলে: id 1, vip: true override, বাকি ডিফল্ট (নাম “নমুনা গ্রাহক”, শহর ঢাকা)।
  3. প্রথম item. id 2, sku SKU-2; test বলেছে qty 2 আর দাম 42000 পয়সা। শুধু এই দুটি এই test-এর কাছে জরুরি।
  4. দ্বিতীয় item. id 3, শুধু দাম 13490 বদলানো, qty ডিফল্ট 1.
  5. Order জোড়া লাগে। buildOrder দেওয়া গ্রাহক আর item নেয়, নিজে মোট হিসাব করে: 2 × 42000 + 1 × 13490 = 97490 পয়সা, ref গ্রাহকের id থেকে BG-9001, অবস্থা ডিফল্ট PENDING.

প্রতিটি test-এ ন্যূনতম ডেটা। “VIP-এর ছাড়” পরীক্ষায় শুধু vip: true আর দাম বলুন; নাম, ফোন, শহর লিখলে পাঠক ভাববেন সেগুলোও জরুরি। API test-এও (পর্ব ৭৫) database-এ শুধু এই test-এর দেখা সারিগুলো রাখুন।

ভাগ করা fixture-এর ফাঁদ। পঞ্চাশটি test একটি বড় fixtures/orders.json পড়ে। একজন নিজের নতুন test-এর জন্য তৃতীয় order-কে PAID করলেন, আর অন্য ফাইলের চারটি test ভাঙল, যারা সেটিকে PENDING ধরে নিয়েছিল। প্রতিটি test নিজের ডেটা factory দিয়ে বানালে এই জোড়া (coupling) থাকে না। ভাগ করা চলে শুধু কখনো না বদলানো ডেটা, যেমন ৬৪টি জেলার নাম।

৬ · Seed দেওয়া এলোমেলো ডেটা আর বাংলা নাম-ঠিকানা

Factory-র ডিফল্ট মান স্থির রাখা যায় (“নমুনা গ্রাহক”), আবার এলোমেলোও করা যায়। এলোমেলো ডেটার লাভ: এমন কোণ ধরা পড়ে, যা আপনি ভাবেননি, যেমন খুব লম্বা নাম বা যুক্তাক্ষরের ঠিকানা। বিপদ: আজ পাস, কাল ফেল, আর কেন তা জানেন না। মাঝের পথ হলো pseudo-random number generator (PRNG) আর তার seed. PRNG আসলে একটি হিসাবের সূত্র; একই seed দিলে সবসময় একই ক্রমে একই “এলোমেলো” সংখ্যা দেয়।

তাস একই নিয়মে একই ক্রমে ফেটালে প্রতিবার একই হাত আসে; বাইরে থেকে এলোমেলো, আসলে নির্ধারিত। Seed হলো সেই শুরুর ক্রম। Test-এ ঠিক এটিই চাই: বৈচিত্র্যময় ডেটা, কিন্তু ফেল হলে হুবহু একই ডেটায় আবার চালানো যায়।

নিচের block-এ একটি পরিচিত ছোট PRNG, mulberry32, একজন বাংলা নামের নকল গ্রাহক বানায়। একই seed দুবার, তারপর দুটি ভিন্ন “ঘড়ির” seed, যা seed না দিলে সাধারণত যা ঘটে তার নকল।

// seed দেওয়া এলোমেলো সংখ্যা: mulberry32 (ছোট, পরিচিত PRNG)
function mulberry32(seed) {
  return function () {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const FIRST = ["রহিম", "করিম", "সুমাইয়া", "তানিয়া", "নাদিয়া", "আরিফ"];
const LAST = ["উদ্দিন", "হক", "আক্তার", "ইসলাম", "চৌধুরী"];
const AREA = ["মিরপুর, ঢাকা", "আগ্রাবাদ, চট্টগ্রাম", "জিন্দাবাজার, সিলেট", "সাহেববাজার, রাজশাহী"];

function customer(rand) {
  const pick = (list) => list[Math.floor(rand() * list.length)];
  const phone = "0170000" + String(Math.floor(rand() * 10000)).padStart(4, "0");
  return `${pick(FIRST)} ${pick(LAST)} | ${phone} | ${pick(AREA)}`;
}

console.log("seed 77, প্রথম run :", customer(mulberry32(77)));
console.log("seed 77, দ্বিতীয় run:", customer(mulberry32(77)));

// "seed ছাড়া" মানে সাধারণত seed আসে ঘড়ি থেকে; এখানে দুটি নকল ঘড়ির মান
const clockRuns = [1762190731042, 1762190755918];
for (const ms of clockRuns) {
  console.log(`ঘড়ির seed ${ms}:`, customer(mulberry32(ms % 2 ** 32)));
}

প্রথম দুই লাইন হুবহু এক: রহিম ইসলাম | 01700004256 | আগ্রাবাদ, চট্টগ্রাম. Seed 77 মানেই এই গ্রাহক, যেকোনো দিন, যেকোনো মেশিনে। শেষ দুই লাইনে seed এসেছে ঘড়ি থেকে, তাই গ্রাহক বদলে গেছে: নাদিয়া আক্তার, সুমাইয়া হক. Test যদি শুধু “সুমাইয়া হক”-এ ফেল করে, seed না জানলে সেই ডেটা আর ফিরে পাবেন না। তাই seed সবসময় দিন, আর এলোমেলো seed নিলে ফেলের বার্তায় সেটি ছাপুন। ফোন নম্বর ইচ্ছা করে 0170000…, যাতে দেখলেই বোঝা যায় নকল।

আসল faker-এ। @faker-js/faker (লেখার সময় 10.6.0) একই কাজ বড় পরিসরে করে, আর faker.seed(77) দিয়ে নির্ধারিত হয়। এতে bn_BD locale আছে, fakerBN_BD নামে import করা যায়। কিন্তু ল্যাবে খুলে দেখা গেল, 10.6.0-এ bn_BD নিজে দেয় শুধু তারিখের মাস আর বারের বাংলা নাম, আর জায়গার নাম: ৪৮০টি উপজেলা বা শহর (location.city), ৬৪টি জেলা (location.county), ৮টি বিভাগ (location.state), দেশের নাম। মানুষের নাম, রাস্তা আর ফোন নম্বর সেখানে নেই, তাই faker ইংরেজি locale-এ ফিরে যায়। Seed 77-এ সরাসরি fakerBN_BD, সত্যিকারের run:

person.fullName()        → Mr. Omar Bogisich
location.streetAddress() → 3547 Old Military Road
location.city()          → টংগিবাড়ী
phone.number()           → 740.741.0207

অর্থাৎ শহর বাংলা, বাকি সব আমেরিকান। সমাধান: নামের তালিকা নিজে দিন, কিন্তু বাছাইয়ের কাজ faker-এর seed দেওয়া helpers.arrayElement-এ রাখুন, যাতে seed-এর নিয়ন্ত্রণ এক জায়গায় থাকে:

import { fakerBN_BD as faker } from '@faker-js/faker';

// bn_BD-তে নাম নেই (লেখার সময়), তাই নিজের ছোট তালিকা
const FIRST = ['রহিম', 'করিম', 'সুমাইয়া', 'তানিয়া', 'নাদিয়া', 'আরিফ'];
const LAST = ['উদ্দিন', 'হক', 'আক্তার', 'ইসলাম', 'চৌধুরী'];

export function buildCustomer(over: Partial<Customer> = {}): Customer {
  seq += 1;
  return {
    id: seq,
    name: `${faker.helpers.arrayElement(FIRST)} ${faker.helpers.arrayElement(LAST)}`,
    phone: `0170000${faker.string.numeric(4)}`,          // স্পষ্ট নকল সংখ্যা
    city: faker.location.city(),
    district: faker.location.county(),
    ...over,
  };
}
export const resetFactories = (s: number) => { seq = 0; faker.seed(s); };
$ npx vitest run tests/factory.test.ts --reporter=verbose
stdout | tests/factory.test.ts > একই seed, একই গ্রাহক
{
  id: 1,
  name: 'আরিফ ইসলাম',
  phone: '01700007107',
  city: 'তারাকান্দা',
  district: 'বরগুনা'
}
stdout | tests/factory.test.ts > override: শুধু যা দরকার তা বলা
BG-9001 করিম আক্তার [ { sku: 'XNR53S', qty: 2, pricePaisa: 42000 } ] 84000
 ✓ tests/factory.test.ts > একই seed, একই গ্রাহক 2ms
 ✓ tests/factory.test.ts > override: শুধু যা দরকার তা বলা 1ms

দুটি জিনিস লক্ষ করুন। উপজেলা তারাকান্দা (ময়মনসিংহে) আর জেলা বরগুনা: faker অংশগুলো আলাদা আলাদা বাছে, তাই একসাথে বাস্তব ঠিকানা হয় না; জেলাভিত্তিক নিয়ম test করলে জোড়া নিজের তালিকা থেকে দিন। আর faker-এর নতুন version-এ তালিকা বদলালে একই seed ভিন্ন ফল দিতে পারে, তাই version lock-এ রাখুন, আর expect এলোমেলো মানে নয়, নিয়মে লিখুন (“মোট = item-এর যোগফল”)। বাংলা সংখ্যা আর তারিখ পর্ব ৫৯-এ।

৭ · Flaky test-এর সাত কারণ — প্রতিটি নিজের হাতে ঘটানো

“মাঝে মাঝে ফেল” ভাগ্যের ব্যাপার নয়; প্রায় সবসময় একটি লুকানো নির্ভরতা থাকে: ঘড়ি, এলোমেলো সংখ্যা, ক্রম, সময়ের প্রতিযোগিতা, network, বা অন্য একটি test. ল্যাবের tests/flaky/ folder-এ সাতটির প্রতিটি সত্যিই ঘটানো হয়েছে।

(ক) সময় আর time zone

একটি order-এর সময় 2026-11-02T20:30:00Z. UTC-তে তখন ২ নভেম্বর রাত সাড়ে আটটা, কিন্তু ঢাকায় (Asia/Dhaka, UTC+6) ৩ নভেম্বর রাত আড়াইটা। ফাংশনটি toLocaleDateString('en-CA') দিয়ে দিন বের করে, কোনো zone না বলে, তাই যে মেশিনে চলে সেই মেশিনের zone ধরে। একই test দুটি TZ-তে, সত্যিকারের run:

$ TZ=Asia/Dhaka npx vitest run tests/flaky/time.test.ts --reporter=verbose
 ✓ ফ্লেকি: মেশিনের zone-এ দিন 13ms
 ✓ ঠিক: Asia/Dhaka স্পষ্ট 0ms
 ✓ ঘড়ি স্থির: delivery আগামীকাল 1ms
   Start at  01:45:39

$ TZ=UTC npx vitest run tests/flaky/time.test.ts --reporter=verbose
 × ফ্লেকি: মেশিনের zone-এ দিন 16ms
   → expected '2026-11-02' to be '2026-11-03' // Object.is equality
 ✓ ঠিক: Asia/Dhaka স্পষ্ট 1ms
 ✓ ঘড়ি স্থির: delivery আগামীকাল 1ms
   Start at  19:45:40

ঢাকার developer-এর ল্যাপটপে সবুজ, UTC-তে চলা CI server-এ লাল (“Start at” লাইনেও ছয় ঘণ্টার পার্থক্য দেখুন)। সমাধান: কোডে { timeZone: 'Asia/Dhaka' } স্পষ্ট, আর “আজ”, “আগামীকাল”-এর হিসাবে test-এ ঘড়ি স্থির (অংশ ৮)।

(খ) Seed ছাড়া এলোমেলো ডেটা

দামের নিয়ম: ৳১০০০-এর বেশি হলে ৫% ছাড়। Test লিখেছেন এমন একজন, যিনি ভেবেছিলেন এলোমেলো ছোট দামে ছাড় কখনো লাগবে না:

test('ফ্লেকি: seed ছাড়া এলোমেলো দাম', () => {
  const qty = faker.number.int({ min: 1, max: 3 });
  const pricePaisa = faker.number.int({ min: 100, max: 400 }) * 100;
  // লেখক ভেবেছিলেন ছাড় কখনো লাগবে না
  expect(cartTotal([{ qty, pricePaisa }])).toBe(qty * pricePaisa);
});

সর্বোচ্চ ৩ × ৳৪০০ = ৳১২০০, তাই ছাড় কখনো কখনো লাগে: qty 3 (সম্ভাবনা ১/৩) আর দাম ৳৩৩৪–৪০০ (৩০১টির ৬৭টি) হলে। মোট (১/৩) × (৬৭/৩০১) ≈ ০.০৭৪২, প্রতি ৫০ বারে গড়ে প্রায় ৩.৭ বার ফেল। অংশ ৮-এ এটি বারবার চালিয়ে ধরা হবে।

(গ) Test-এর ক্রম আর ভাগ করা অবস্থা

Module-এর স্তরে রাখা একটি cart array, যা ফাইলের সব test দেখে। ফাইলের ক্রমে “খালি cart” আগে, “চাল যোগ” পরে, তাই দুটিই পাস। Vitest-কে ক্রম এলোমেলো করতে বললে (--sequence.shuffle), seed অনুযায়ী ক্রম বদলায়। দশটি seed চালানো হলো; seed 1, 2, 4–8, 10-এ দুটিই পাস, seed 3 আর 9-এ:

$ npx vitest run tests/flaky/order.test.ts --sequence.shuffle --sequence.seed=3 --reporter=verbose
 ✓ tests/flaky/order.test.ts > চাল যোগ করলে ১টি 1ms
 × tests/flaky/order.test.ts > খালি cart-এ ০টি 3ms
   → expected 1 to be +0 // Object.is equality
      Tests  1 failed | 1 passed (2)

বাস্তবে ক্রম বদলায় নতুন test যোগ হলে বা -t দিয়ে একটি test একা চালালে। নিচের block-এ একই ঘটনা একটি ছোট শিক্ষার runner-এ, সাথে সমাধান।

// শিক্ষার মডেল: ছোট একটি test runner, আসল Vitest নয়
function runSuite(label, tests, beforeEach = () => {}) {
  const out = [];
  for (const [name, fn] of tests) {
    beforeEach();
    try { fn(); out.push(`✓ ${name}`); } catch (e) { out.push(`× ${name}: ${e.message}`); }
  }
  console.log(label.padEnd(18), out.join("   "));
}
const expectEq = (got, want) => { if (got !== want) throw new Error(`পেলাম ${got}, চাই ${want}`); };

// ভুল: সব test একই module-level cart ভাগ করে
let cart = [];
const add = (sku) => cart.push(sku);
const A = ["খালি cart = 0", () => expectEq(cart.length, 0)];
const B = ["চাল যোগ = 1", () => { add("RICE-5KG"); expectEq(cart.length, 1); }];

cart = []; runSuite("ক্রম A, B:", [A, B]);
cart = []; runSuite("ক্রম B, A:", [B, A]);
cart = []; runSuite("শুধু A:", [A]);

// ঠিক: প্রতিটি test-এর আগে নতুন cart
const fresh = () => { cart = []; };
runSuite("ঠিক, ক্রম A, B:", [A, B], fresh);
runSuite("ঠিক, ক্রম B, A:", [B, A], fresh);

ক্রম A, B-তে দুটিই ✓. ক্রম B, A-তে “চাল যোগ” আগে চলে cart-এ একটি জিনিস রেখে যায়, তাই “খালি cart” পায় পেলাম 1, চাই 0. শুধু A একা চালালে আবার ✓: এজন্যই flaky test নিয়ে “আমার মেশিনে তো পাস করে” কথাটি এত শোনা যায়। শেষ দুই লাইনে প্রতিটি test-এর আগে নতুন cart (beforeEach-এর কাজ), আর এখন ক্রম যা-ই হোক, দুটিই ✓. ল্যাবের আসল সমাধানও এটিই: module-level array বাদ দিয়ে createCart(), প্রতিটি test নিজেরটি বানায়; তিনটি ভিন্ন shuffle seed-এ দুটিই পাস করেছে। মূল নীতির নাম test isolation.

Mock-এর ইতিহাসও ভাগ করা অবস্থা। Module-এর স্তরে একটি const sendSms = vi.fn(), আর দুটি test প্রতিটিতে একটি order দিয়ে toHaveBeenCalledTimes(1) দেখে। Vitest 5.1.0-এর ডিফল্টে দুটিই পাস করল, কারণ Vitest 5-এ clearMocks ডিফল্টে চালু: প্রতিটি test-এর আগে mock-এর ডাকার ইতিহাস মুছে যায়। একই ফাইল config-এ clearMocks: false দিয়ে (Vitest 4-এর পুরোনো ডিফল্টের মতো) চালালে, সত্যিকারের run:

 ✓ tests/leak2/mockleak.test.ts > প্রথম order-এ একটি SMS 1ms
 × tests/leak2/mockleak.test.ts > দ্বিতীয় order-এ একটি SMS 2ms
   → expected "vi.fn()" to be called 1 times, but got 2 times

অর্থাৎ Vitest-এর version বা config বদলালে একই suite ভিন্ন ফল দেয়। এই পর্বের বাকি সব run Vitest 5-এর ডিফল্ট config-এ (clearMocks চালু)। নিজের প্রজেক্টে মানটি config-এ স্পষ্ট লিখুন, আর মনে রাখুন এটি শুধু ডাকার ইতিহাস মোছে; module-level array বা database-এর সারি নয়।

(ঘ) Async: await ভুলে যাওয়া আর উত্তরের প্রতিযোগিতা

Coupon যাচাইয়ের একটি async ফাংশন আছে, যাতে বাগ: অচেনা কোডে error হওয়ার কথা, কিন্তু চুপচাপ পুরো দাম ফেরত দেয়। তিনটি test, সত্যিকারের run:

test('১. rejects-এ await নেই', () => {
  expect(applyCoupon('FAKE99', 50000)).rejects.toThrow();
});
test('২. then-এর ভেতরে expect, return নেই', () => {
  applyCoupon('EID10', 50000).then((v) => expect(v).toBe(40000));   // ভুল প্রত্যাশা
});
test('৩. পরের একটি নিরীহ test', async () => {
  await new Promise((r) => setTimeout(r, 20));
  expect(1 + 1).toBe(2);
});
 × ১. rejects-এ await নেই 19ms
   → promise resolved "50000" instead of rejecting
   → Promise returned by `expect(actual).rejects.toThrow()` was not awaited. …
 ✓ ২. then-এর ভেতরে expect, return নেই 0ms
 ✓ ৩. পরের একটি নিরীহ test 27ms

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
AssertionError: expected 45000 to be 40000 // Object.is equality
 ❯ tests/flaky/await.test.ts:8:53
The last test to run before this error was "৩. পরের একটি নিরীহ test".
 Test Files  1 failed (1)
      Tests  1 failed | 2 passed (3)
     Errors  1 error

Vitest 5 প্রথম ভুলটি নিজেই ধরেছে: await ছাড়া .rejects এখন স্পষ্ট বার্তাসহ ফেল। দ্বিতীয়টি ধরেনি। Test ২ সবুজ (০ ms!), কারণ ফাংশন promise ফেরত দেয়নি, তাই Vitest অপেক্ষা করেনি। ভুল expect পরে চলেছে, আর দোষ পড়েছে নির্দোষ test ৩-এর ঘাড়ে। বড় suite-এ লাল দাগ কোন test-এ পড়বে, তা তখন সময়ের ওপর নির্ভর করে। নিয়ম: async কাজ থাকলে test async, প্রতিটি promise-এ await; typescript-eslint-এর no-floating-promises এটি আগেই ধরে।

উত্তরের প্রতিযোগিতা (race condition). খোঁজার ঘরে কেউ “চা” লিখলেন, তারপর “চাল”। দুটি অনুরোধ যায়; যেটি শেষে পৌঁছায়, বাগওয়ালা কোড সেটিই দেখায়। MSW handler-এ ০ থেকে ৪০ ms-এর এলোমেলো দেরি দিয়ে (network-এর মতো), প্রতিটি test ৫০ বার চালানো হলো:

$ npx vitest run tests/flaky/race.test.ts --repeats=49 --reporter=verbose
 × বাগ: 'চা' তারপর 'চাল' লিখলে 'চাল' দেখায় 2380ms (repeat x49)
   → expected 'চা' to be 'চাল' // Object.is equality      (মোট ৬ বার)
 ✓ ঠিক: 'চা' তারপর 'চাল' লিখলে 'চাল' দেখায় 2223ms (repeat x49)
 FAIL … (2nd repeat, 5th repeat, 12th repeat, 24th repeat, 33rd repeat, 48th repeat)

প্রথম run-এ ৫০ বারে ৬ বার, দ্বিতীয়টিতে ৯ বার ফেল। এখানে কোডেই বাগ, ব্যবহারকারীও মাঝে মাঝে ভুল ফল দেখবেন। কোডের সমাধান: নতুন অনুরোধের আগে পুরোনোটি AbortController দিয়ে বাতিল (পর্ব ১৩-এর cleanup-এর ধারণা)। Test-এর সমাধান: এলোমেলো দেরির বদলে নির্দিষ্ট দেরি (প্রথমটি ৩০ ms, দ্বিতীয়টি ০), যাতে বাগ থাকলে প্রতিবার ফেল হয়।

(ঙ) বাইরে বেরিয়ে যাওয়া network

একটি test ডেলিভারি চার্জ জানতে http://127.0.0.1:47782-এ dev API ডাকে, আর এই config-এ MSW নেই। Developer-এর মেশিনে dev server চালু থাকলে, সত্যিকারের run:

== সার্ভার চালু
 ✓ tests/leak/rates.test.ts > ডেলিভারি চার্জ: ঢাকার ভেতরে ৬০ টাকা 26ms
== সার্ভার বন্ধ (যেমন CI-তে)
 × tests/leak/rates.test.ts > ডেলিভারি চার্জ: ঢাকার ভেতরে ৬০ টাকা 19ms
TypeError: fetch failed
Caused by: Error: connect ECONNREFUSED 127.0.0.1:47782

Test আসলে যাচাই করছিল “dev server চালু আছে কি না”, ডেলিভারি চার্জের নিয়ম নয়। সমাধান অংশ ৩-এরটিই: MSW সব test-এ, onUnhandledFrame: 'error'.

(চ) Animation আর আসল সময়ের অপেক্ষা

বাজারঘরের toast “বন্ধ” চাপলে ৯৮০ ms-এর fade-out শেষে DOM থেকে সরে, আর test waitForElementToBeRemoved-এর ডিফল্ট ১০০০ ms সীমায় অপেক্ষা করে; হাতে মাত্র ২০ ms. ফাঁকা মেশিনে ২০ বারে ২০ বারই পাস। তারপর CI-এর ব্যস্ত মেশিনের নকল করতে core-এর সমান সংখ্যক CPU-খাওয়া Node process (নিজের, PID ধরে পরে বন্ধ) চালিয়ে ৫০ বার:

$ npx vitest run tests/flaky/animation.test.tsx -t "ফ্লেকি" --repeats=49 --reporter=verbose
 × ফ্লেকি: আসল ঘড়িতে animation শেষের অপেক্ষা 50472ms (repeat x49)
   → Timed out in waitForElementToBeRemoved.        (৫০ বারে ৫ বার)

$ npx vitest run tests/flaky/animation.test.tsx -t "ঠিক" --repeats=49     # একই চাপের মধ্যে
 ✓ ঠিক: নকল ঘড়ি, সময় নিজে এগিয়ে দেওয়া 154ms (repeat x49)

এগুলো একটি মেশিনের একটি run, সাধারণ ফল নয়; কিন্তু ধরনটি বাস্তব: চাপ বাড়লে timer দেরিতে চলে, আর “প্রায় সমান” দুটি সময়ের প্রতিযোগিতা হেরে যায়। Component test-এ সমাধান নকল ঘড়ি (অংশ ৮)। E2E-তে Playwright-এর page.emulateMedia({ reducedMotion: 'reduce' }) বা animation বন্ধের CSS, আর auto-wait করা assertion (পর্ব ৭৬)। কখনো sleep(1000) নয়।

(ছ) সমান্তরাল test আর একই database সারি

Vitest ডিফল্টে আলাদা ফাইল আলাদা worker process-এ একসাথে চালায়। দুটি ফাইল, stock-a আর stock-b, একই database file-এর একই সারি RICE-5KG ব্যবহার করে: stock ১০ বসায়, একটি reserve করে, আশা করে ৯. প্রতিটি ফাইল ৫০ বার, একসাথে, চারবার চালানো হলো। প্রতিবারই ১০০টি run-এ ১টি ফেল:

$ npx vitest run tests/db --repeats=49 --reporter=verbose
 ✓ tests/db/stock-b.test.ts > b: একটি reserve করলে stock ১০ থেকে ৯ 1539ms (repeat x49)
 × tests/db/stock-a.test.ts > a: একটি reserve করলে stock ১০ থেকে ৯ 1602ms (repeat x49)
   → expected 8 to be 9 // Object.is equality
 FAIL  tests/db/stock-a.test.ts > … (46th repeat)

$ npx vitest run tests/db --repeats=49 --no-file-parallelism
      Tests  2 passed (2)

8 মানে অন্য ফাইলের reserve ঠিক মাঝখানে একই সারিতে পড়েছে। --no-file-parallelism সবুজ করে, কিন্তু সেটি ধীর করে লুকানো। আসল সমাধান: প্রতিটি test নিজের সারি (RICE-5KG- আর একটি randomUUID()), বা পর্ব ৭৫-এর মতো worker-প্রতি database বা rollback হওয়া transaction. নিজের সারির সংস্করণ তিনবার ১০০টি করে run-এ একবারও ফেল করেনি। এটি production-এর আসল race-এরও আয়না: একই stock-এ দুজন ক্রেতা (পর্ব ৪৩)।

৮ · Flaky খোঁজা, quarantine, নকল ঘড়ি আর retry-এর বিপদ

একটি test একবার লাল হলো, আবার চালালে সবুজ। প্রথম কাজ অনুমান নয়, মাপা। Vitest 5-এ --repeats=N প্রতিটি test প্রথমবারের পরে আরও N বার চালায়, তাই --repeats=49 মানে মোট ৫০ বার, একই process-এ, কয়েক সেকেন্ডে। যে flake process-এর বাইরের জিনিসে নির্ভর করে (ফাইলের সমান্তরালতা, port, TZ), তার জন্য shell-এর loop, যেমন for i in $(seq 50); do npx vitest run tests/x.test.ts || echo FAIL $i; done. Playwright-এর সমতুল্য --repeat-each (পর্ব ৭৬)। নিচে অংশ ৭(খ)-এর test খোঁজা থেকে সারানো পর্যন্ত:

  1. একবার1 passedসব ঠিক?
  2. ৫০ বার4 / 50--repeats
  3. বার্তা পড়া111900কোন ডেটা
  4. কারণ≈ 7.4%হিসাব মেলে
  5. সারানো0 / 50সীমার test
  6. Retry নয়--retry=2লুকায়
প্রথম runসত্যিকারের
✓ ফ্লেকি: seed ছাড়া এলোমেলো দাম 1ms
Tests  1 passed (1)
--repeats=49তিনটি আলাদা run
run ১: ফেল 3rd, 13th, 29th, 40th repeat → 4/50
run ২: ফেল 27th, 29th, 36th, 46th       → 4/50
run ৩: ফেল 26th, 34th, 43rd, 44th       → 4/50
ফেলের বার্তাrun ১, 3rd repeat
expected 106305 to be 111900
111900 > 100000 → ৫% ছাড় লেগেছে
111900 × 0.95 = 106305
কারণহিসাব
ছাড় লাগে শুধু qty = 3 আর দাম ৳৩৩৪–৪০০ হলে
P = (1/3) × (67/301) ≈ 0.0742
৫০ বারে প্রত্যাশা ≈ 3.7  ↔  দেখা গেল 4, 4, 4
সারানো test--repeats=49
beforeEach(() => faker.seed(77))
✓ সীমার ঠিক নিচে: ৳১০০০-এ ছাড় নেই   100000 → 100000
✓ সীমার ঠিক ওপরে: ৳১০০১-এ ৫% ছাড়    100100 → 95095
(repeat x49) Tests passed
পুরোনো test, --retry=2JSON report
✓ ফ্লেকি: seed ছাড়া এলোমেলো দাম (repeat x49)
status: "passed"
failureMessages: ["AssertionError: expected 102600 to be 108000 …"]
  1. একবার চালানো। সবুজ। একবারের সবুজ প্রায় কিছুই প্রমাণ করে না, যখন ফেলের সম্ভাবনা ৭%-এর মতো।
  2. মাপা। --repeats=49, মোট ৫০ বার। তিনটি আলাদা run-এ প্রতিবার ৪টি ফেল, প্রতিবার ভিন্ন repeat-এ। এখন সমস্যাটি সংখ্যায়: “৫০ বারে প্রায় ৪ বার”।
  3. বার্তা পড়া। প্রত্যাশা 111900, পাওয়া 106305. 111900 পয়সা = ৳১১১৯, যা ৳১০০০-এর বেশি; 111900 × 0.95 = 106305, অর্থাৎ ছাড় ঠিকভাবেই লেগেছে। ভুল test-এর ধারণায়, কোডে নয়।
  4. হিসাব মেলানো। ছাড় লাগার সম্ভাবনা (১/৩) × (৬৭/৩০১) ≈ ০.০৭৪২; ৫০ বারে প্রত্যাশা প্রায় ৩.৭, দেখা গেল ৪, ৪, ৪. সংখ্যা মিলছে, তাই কারণ নিশ্চিত।
  5. সারানো। এলোমেলো ডেটার বদলে নিয়মের সীমার দুই পাশে দুটি স্পষ্ট test: ১০০০০০ পয়সায় ছাড় নেই, ১০০১০০-এ 95095. ৫০ বার চালিয়ে একবারও ফেল নেই। Faker থাকলেও seed দেওয়া।
  6. Retry দিয়ে ঢাকা নয়। পুরোনো test --retry=2 দিয়ে চালালে terminal-এ শুধু ✓. JSON report-এ সেই test-এর status passed, অথচ ভেতরে একটি failure message লুকানো। Retry সমস্যা মুছে দেয়নি, শুধু চোখের আড়াল করেছে।

কত বার চালাবেন? একটি test প্রতি run-এ স্বাধীনভাবে p সম্ভাবনায় ফেল করলে n বারে অন্তত একবার ফেল দেখার সম্ভাবনা 1 − (1 − p)n. p = 0.0742, n = 50 হলে প্রায় ০.৯৮. কিন্তু p = 0.01 হলে ৫০ বারে মাত্র প্রায় ৩৯.৫%; ৯৫% নিশ্চয়তায় n ≥ ln(0.05) / ln(0.99) ≈ ২৯৮.১, অর্থাৎ ২৯৯ বার। উল্টো দিকে, ২০০টি test-এর প্রতিটি ০.৫% flaky হলে পুরো run সবুজ হওয়ার সম্ভাবনা 0.995200 ≈ ০.৩৬৭: প্রায় দুই-তৃতীয়াংশ CI run লাল, যদিও আলাদা করে প্রতিটি test “প্রায় নির্ভরযোগ্য”।

নকল ঘড়ি। সময়ের ওপর নির্ভর করা কোডের জন্য Vitest-এ vi.useFakeTimers() আর vi.setSystemTime() (পর্ব ৭৩-এ এর ভিত্তি)। নকল ঘড়ি চালু থাকলে Date আর setTimeout আপনার কথামতো চলে: সময় এগোয় শুধু যখন আপনি vi.advanceTimersByTime বলেন। ল্যাবের দুটি test এভাবে:

afterEach(() => vi.useRealTimers());

test('ঘড়ি স্থির: delivery আগামীকাল', () => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date('2026-11-03T17:59:00Z'));   // ঢাকায় রাত ১১:৫৯
  expect(deliveryLabel(new Date('2026-11-04T17:59:00Z'))).toBe('আগামীকাল');
});

test('ঠিক: নকল ঘড়ি, সময় নিজে এগিয়ে দেওয়া', () => {
  vi.useFakeTimers();
  render(<Toast />);
  fireEvent.click(screen.getByText('বন্ধ'));
  act(() => vi.advanceTimersByTime(980));
  expect(screen.queryByRole('status')).toBeNull();
});

প্রথম test-এ সময় ইচ্ছা করে ঢাকার মাঝরাতের এক মিনিট আগে, কারণ বাগ সেখানেই লুকায়। দ্বিতীয়টি CPU-র চাপেও ৫০ বারে ৫০ বার পাস করেছে (অংশ ৭(চ))। afterEach-এ আসল ঘড়ি ফেরানো জরুরি, নইলে নকল ঘড়ি পরের test-এ ঢুকে নতুন flake বানাবে। নকল ঘড়ির সাথে findBy… বা MSW-এর delay মেশালে সাবধান; তারাও timer ব্যবহার করে।

Quarantine, তারপর সারানো। আজই সারানোর সময় না থাকলে flake-টি মূল suite থেকে আলাদা করুন, যেমন *.quarantine.test.ts নামে, যা মূল config-এর exclude-এ আর একটি আলাদা job-এ চলে। শর্ত: প্রতিটির issue আর মালিক, একটি সময়সীমা (ধরুন দুই সপ্তাহ), আর তারপর সারানো বা মুছে ফেলার স্পষ্ট সিদ্ধান্ত। CI-তে এর ব্যবস্থা পর্ব ৮১-এ।

Retry কেন সমাধান নয়। Vitest-এর --retry বা Playwright-এর retries ফেল হলে আবার চালায়, একবার পাস করলেই সবুজ। অংশ ৭(ঘ)-এর race-এ test flaky ছিল কোডের সত্যিকারের বাগের জন্য, আর retry ঠিক সেটিই চাপা দিত। সৎ ব্যবহার একটিই: নিয়ন্ত্রণের বাইরের ক্ষণিক সমস্যা, আর তখনও “retry-তে পাস” আলাদা গুনে দেখা; --retry.condition দিয়ে নির্দিষ্ট error-এ সীমিত করা যায়।

৯ · Contract test — mock-কে সৎ রাখা, আর একটি চেকলিস্ট

প্রতিটি mock একটি দাবি: “বাইরের সেবা এভাবে উত্তর দেয়”। ছয় মাস পরে সেবাটি একটি field-এর নাম বদলাল, আর আপনার MSW handler এখনো পুরোনো আকার দেখায়। সব test সবুজ, production লাল।

এর উত্তর consumer-driven contract test. ধরুন বাজারঘরের frontend (consumer) আর backend (provider) দুটি আলাদা দল বানায়। Consumer-এর test চলার সময় একটি চুক্তির ফাইল লেখা হয়: “আমি GET /api/products চাই, আর উত্তরে প্রতিটি পণ্যে id, name আর পূর্ণসংখ্যার pricePaisa লাগবে”। Provider-এর দিকে সেই একই ফাইল আসল server-এর বিরুদ্ধে চালানো হয়। Provider যদি pricePaisa সরিয়ে price করে, তাহলে provider-এর CI ভাঙে, deploy-এর আগেই। এভাবে consumer-এর mock আর provider-এর আসল আচরণ একই চুক্তিতে বাঁধা থাকে।

Pact সম্পর্কে একটি সতর্ক নোট। এই ধরনের সবচেয়ে পরিচিত open-source টুল Pact; JavaScript-এর package @pact-foundation/pact (লেখার সময় npm-এ 17.1.4). এটি consumer-এর test থেকে চুক্তি (একে “pact” বলে) বানায় আর provider-এর দিকে যাচাই করে; দলগুলোর মধ্যে চুক্তি ভাগের জন্য একটি broker-এর ধারণাও আছে। এই পর্বের ল্যাবে Pact চালানো হয়নি, তাই এর API এখানে দেখানো হচ্ছে না; ব্যবহারের আগে docs.pact.io-র বর্তমান নির্দেশনা আর আপনার version মিলিয়ে নেবেন। একই repository-র ছোট দলে (পর্ব ৩৭) হালকা পথই প্রায়ই যথেষ্ট: একটি Zod schema বা OpenAPI চুক্তি, যার বিরুদ্ধে MSW handler আর আসল server দুটির উত্তরই যাচাই হয়।

লক্ষ করুন, কোর্সের আগের contract test শব্দটির অর্থ আলাদা: একই test একটি interface-এর সব বাস্তবায়নে চালানো। লক্ষ্য একই, নকল আর আসল যেন আলাদা না হয়ে যায়।

আরও গভীরে: Martin Fowler-এর “Mocks Aren't Stubs” প্রবন্ধ test double-এর দুই ঘরানা, classicist আর mockist, আলাদা করে দেখায়। “যা আপনার নয়, তা সরাসরি mock না করে নিজের পাতলা adapter-এর আড়ালে রাখুন” পরামর্শটি সাধারণত Freeman আর Pryce-এর “Growing Object-Oriented Software, Guided by Tests” (২০০৯)-এর সাথে যুক্ত। Flaky test-এর কারণ নিয়ে বহুল উদ্ধৃত গবেষণা Luo ও সহকর্মীদের “An Empirical Analysis of Flaky Tests” (FSE ২০১৪), যেখানে async অপেক্ষা, concurrency আর test-এর ক্রম বড় শ্রেণি হিসেবে এসেছে।

Mock আর test data-র চেকলিস্ট

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

  • Mock, stub আর fake? Stub তৈরি উত্তর দেয়, mock ডাকার ধরন যাচাই করে, fake সহজ কিন্তু কাজ করা বাস্তবায়ন।
  • নিজের database কেন mock করবেন না? তাহলে SQL, schema আর constraint কখনো চলে না; ভুল column-এর নামেও test সবুজ থাকে।
  • MSW কোথায় নকল করে? Network-এর সীমানায়; আপনার কোড অপরিবর্তিত।
  • Flaky test-এর তিনটি সাধারণ কারণ? Async অপেক্ষা বা race, test-এর ক্রম ও ভাগ করা অবস্থা, আর সময় বা এলোমেলো ডেটা।
  • ৫০ বারে ৪ বার ফেল করা test-এ retry দেওয়া কি ঠিক? না; আগে কারণ খুঁজুন, প্রয়োজনে সময়সীমাসহ quarantine, কারণ retry আসল বাগও লুকাতে পারে।

১০ · AI-এর যুগে mock আর test data

AI-এর লেখা test প্রায় সবসময় পাস করে, কারণ সে প্রায়ই কোডের বর্তমান আচরণ দেখে প্রত্যাশা লেখে, আর কঠিন জায়গায় mock বসায়। এই পর্বের ল্যাবেই জ্যান্ত উদাহরণ পেলেন: MSW 2-এর onUnhandledRequest, যা version 3-এ নীরবে উপেক্ষিত।

AI-এর ভুলকীভাবে ধরবেন
MSW 2-এর import (from 'msw') আর onUnhandledRequestnpm ls msw, migration guide; setup ফাইলে tsc
Prisma client বা নিজের repository vi.mock করাMock সরিয়ে test database-এ চালান; ফেল করলে test আগে কিছুই যাচাই করছিল না
কোডের বর্তমান ফল দেখে প্রত্যাশা লেখা (বাগসহ)Expect-এর প্রতিটি সংখ্যা নিজে হিসাব করুন, নিয়মের সীমায় test চান
faker seed ছাড়া, new Date() সরাসরি--repeats=49 আর TZ=UTC দিয়ে চালান
Flaky test সারাতে sleep, timeout বাড়ানো বা retryকারণ জিজ্ঞেস করুন; নকল ঘড়ি বা auto-wait চান
নেই এমন option বা package (যেমন বানানো faker locale-এর নাম)npm view, package-এর ভেতরের ফাইল খুলে দেখা
Test-এ আসল key বা আসল ফোন নম্বরDiff-এ sk_, 01[3-9] খুঁজুন; নকল নম্বর 0170000…

AI-কে version, সীমানা আর যাচাইয়ের পদ্ধতি স্পষ্ট বলে দিন। আসল গ্রাহকের ডেটা, ফোন নম্বর বা key prompt-এ দেবেন না।

আমি Node 24, Vitest 5.1, MSW 3.0.3 (import 'msw/http', 'msw/node'), @testing-library/react 16 আর @faker-js/faker 10.6 ব্যবহার করছি। নিচের ProductList component-এর জন্য test লিখে দাও। শর্ত: ১. MSW 3-এর API: server.listen({ onUnhandledFrame: 'error' }), afterEach-এ resetHandlers. MSW 2-এর onUnhandledRequest বা 'msw' থেকে import লিখবে না। ২. চারটি test: সফল, 500, 300ms দেরি (delay 'msw/utils/delay' থেকে), আর HttpResponse.error(). প্রতিটি শুধু নিজের handler server.use দিয়ে বসাবে। ৩. Test data একটি factory থেকে, faker.seed(77) সহ। মানুষের নাম আমার দেওয়া বাংলা তালিকা থেকে, ফোন নম্বর 0170000 দিয়ে শুরু, যাতে স্পষ্ট নকল হয়। ৪. আমার নিজের API client বা database mock করবে না। কোনো sleep বা retry নয়; সময় লাগলে vi.useFakeTimers. ৫. প্রতিটি expect-এর সংখ্যা কোথা থেকে এলো, এক লাইনে হিসাব দেখাও। ৬. তুমি test চালাওনি, তাই "সব পাস করেছে" বলবে না; কোন কমান্ডে আমি ৫০ বার চালিয়ে flake খুঁজব, সেটি লিখে দাও।

AI-এর লেখা test মেলানোর চেকলিস্ট

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

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

প্র ০১Flaky test তো “মাঝে মাঝে” ফেল করে, বেশিরভাগ সময় পাস। এতে বড় দলের এত ক্ষতি হয় কেন?

একটি test-এর কাজ একটি প্রশ্নের নির্ভরযোগ্য উত্তর দেওয়া: “এই পরিবর্তন কি কিছু ভাঙল?” Flaky test সেই উত্তরকে মুদ্রা ছোড়ায় নামিয়ে আনে। লাল দেখলে developer আর জানেন না, বাগ আছে নাকি test-এর মেজাজ খারাপ। কয়েকবার “আবার চালাও, সবুজ হবে” কাজ করার পর মানুষ লাল দেখলেই আবার চালাতে শেখেন। তারপর একদিন সত্যিকারের বাগ লাল দেখায়, আর সেটিও “flaky” ভেবে আবার চালানো হয়, কপালগুণে সবুজ হয়, আর বাগ production-এ যায়।

বড় প্রতিষ্ঠানগুলো এ নিয়ে প্রকাশ্যে লিখেছে। ২০১৬ সালে Google Testing Blog-এ John Micco-র একটি লেখায় জানানো হয়েছিল, তাঁদের বিপুল test-এর একটি উল্লেখযোগ্য অংশে কোনো না কোনো মাত্রার flakiness দেখা যায়, আর মোট run-এর প্রায় দেড় শতাংশের মতো flaky ফল দেয়; সংখ্যাগুলো সেই সময়ের আর সেই পরিবেশের, তবু ধারণাটি পরিষ্কার: বড় পরিসরে ছোট শতাংশও প্রতিদিন হাজার হাজার বিভ্রান্তিকর লাল দাগ।

গণিতটিও নির্মম। এই পর্বের হিসাবে দেখেছেন, ২০০টি test-এর প্রতিটি মাত্র ০.৫% flaky হলে পুরো suite সবুজ হওয়ার সম্ভাবনা প্রায় ৩৬.৭%. অর্থাৎ কোনো একটি test-কে আলাদা করে দেখলে নির্দোষ মনে হয়, কিন্তু একসাথে তারা দলকে প্রায় প্রতিটি pull request-এ আটকায়। এর খরচ তিন রকম: মানুষের সময় (আবার চালানো, তদন্ত), CI-এর যন্ত্রের সময়, আর সবচেয়ে দামি, আস্থা।

আরেকটি সূক্ষ্ম ক্ষতি: flaky test প্রায়ই আসল বাগের লক্ষণ। এই পর্বের খোঁজার ঘরের race-এ test flaky ছিল কারণ কোডেই race ছিল; ব্যবহারকারীও কখনো কখনো ভুল ফল দেখতেন। Flake-কে “test-এর সমস্যা” বলে উপেক্ষা করলে সেই বাগও উপেক্ষিত হয়।

মূল উপলব্ধি: Test suite-এর মূল্য তার নির্ভরযোগ্যতায়। একটি flake শুধু একটি test-এর সমস্যা নয়, পুরো suite-এর ওপর আস্থার ক্ষয়; তাই একে বাগের মতোই গুরুত্ব দিয়ে মাপা, ধরা আর সারানো দরকার।

প্র ০২“সব সহযোগী mock করুন” আর “যতটা সম্ভব আসল জিনিস ব্যবহার করুন”, দুই ঘরানার তর্কটি কী? ফুল-স্ট্যাক ওয়েব অ্যাপে কোন দিকে ঝোঁকা যুক্তিসংগত?

২০০০-এর দশকের শুরুতে TDD-র চর্চায় দুটি ধারা স্পষ্ট হয়। একদল, পরে যাঁদের “London school” বা mockist বলা হয়, প্রতিটি class-কে সহযোগীদের mock দিয়ে আলাদা করে test করতেন আর যাচাই করতেন কোন বার্তা কাকে পাঠানো হলো; Steve Freeman আর Nat Pryce-এর ২০০৯ সালের বই “Growing Object-Oriented Software, Guided by Tests” এই ধারার প্রভাবশালী রচনা। অন্য দল, “Detroit” বা classicist, যেখানে সম্ভব আসল object ব্যবহার করে ফলাফলের অবস্থা যাচাই করতেন। Martin Fowler-এর “Mocks Aren't Stubs” প্রবন্ধ পার্থক্যটি বহু পাঠকের কাছে পরিচিত করে।

দুই পক্ষেরই যুক্তি আছে। Mockist test খুব দ্রুত, আর নকশার সমস্যা আগে দেখায়: সাতটি সহযোগী mock করতে হলে বোঝা যায় class-টি বেশি কাজ করছে। খরচ: test ভেতরের গঠনের সাথে বাঁধা, refactor-এ ভাঙে, আর mock-এর দাবি মিথ্যা হলেও সবুজ থাকে। Classicist test আচরণ যাচাই করে, refactor-এ টেকে, কিন্তু ধীর হতে পারে।

ফুল-স্ট্যাক ওয়েব অ্যাপে বেশিরভাগ ঝুঁকি থাকে সীমানায়: SQL আর schema, HTTP-র আকার, serialization, auth middleware. এই অংশগুলো mock করলে ঠিক সেই জায়গাগুলোই test-এর বাইরে চলে যায়, যেখানে বাগ সবচেয়ে বেশি। আবার আধুনিক টুল আসল জিনিসকে সস্তা করেছে: portable বা container-এ চলা PostgreSQL কয়েক সেকেন্ডে ওঠে, truncate বা worker-প্রতি database প্রতিটি test-কে পরিষ্কার রাখে (পর্ব ৭৫)। তাই অনেক দল এখন classicist-এর দিকে ঝোঁকে: নিজের সবকিছু আসল, শুধু network-এর বাইরের জিনিস নকল, আর সেটিও HTTP স্তরে।

তবু এটি ধর্ম নয়। জটিল হিসাবের খাঁটি ফাংশন (ছাড়, শিপিং চার্জ) unit test-এ, কোনো mock ছাড়াই, হাজারটা কোণসহ চালানো সবচেয়ে ভালো। আর বাইরের সেবার সাথে কথা বলা একটি পাতলা adapter-কে mock করাও যুক্তিসংগত, যদি adapter নিজে অন্য কোথাও contract বা integration test-এ যাচাই হয়।

মূল উপলব্ধি: প্রশ্ন “mock করব কি না” নয়, “বাগ কোথায় লুকায়”। যেখানে বাগ লুকায়, সেখানে আসল জিনিস চালান; যেখানে নকল ছাড়া test অসম্ভব বা অনিশ্চিত, শুধু সেখানে নকল।

প্র ০৩২০২২ সালের শুরুতে মূল faker.js package নিয়ে কী ঘটেছিল, আর test data-র একটি library থেকে supply chain নিয়ে কী শেখার আছে?

Faker-এর ধারণা পুরোনো; Perl আর Ruby-র জগতে একই নামের library বহু আগে থেকে ছিল, আর JavaScript-এর faker.js দীর্ঘদিন ধরে খুব জনপ্রিয় ছিল, সাপ্তাহিক download লাখের ঘরে বলে জানা যায়। ২০২২ সালের জানুয়ারিতে এর মূল রক্ষণাবেক্ষণকারী, যিনি জনপ্রিয় colors.js-ও রক্ষণাবেক্ষণ করতেন, প্রকাশ্য অসন্তোষের প্রেক্ষিতে দুটি project-এ ইচ্ছাকৃত পরিবর্তন প্রকাশ করেন বলে ব্যাপকভাবে প্রতিবেদিত হয়েছিল: faker.js-এর repository প্রায় খালি করে দেওয়া হয়, আর colors.js-এর একটি নতুন version এমন কোড পায় যা চালালে অর্থহীন লেখা অবিরাম ছাপে। যেসব project version না আটকে নতুনটি তুলে নিয়েছিল, তাদের build আর test হঠাৎ ভেঙে যায়।

এরপর সম্প্রদায়ের একদল developer project-টি fork করে নতুন নামে চালু করেন: @faker-js/faker, যা এই পর্বে ব্যবহার করা হয়েছে। এখন এটি একটি দলের রক্ষণাবেক্ষণে, নিয়মিত release হয়।

শিক্ষা কয়েকটি। প্রথমত, devDependency-ও supply chain-এর অংশ। Test-এর library production-এ যায় না বলে অনেকে নিশ্চিন্ত থাকেন, কিন্তু সেটি আপনার CI-তে, developer-এর মেশিনে আর build-এ চলে, যেখানে secret আর source code থাকে। দ্বিতীয়ত, lock file (package-lock.json) আর npm ci বিপদের সময় প্রথম ঢাল: যা কাল চলেছে, আজও হুবহু তাই চলবে। তৃতীয়ত, test data-র library বিশেষ করে version-সংবেদনশীল, কারণ seed দেওয়া ফলও ডেটার তালিকার ওপর নির্ভর করে; নতুন version-এ নাম যোগ হলে একই seed ভিন্ন গ্রাহক দেয়। তাই test-এর প্রত্যাশা এলোমেলো মানের ওপর নয়, নিয়মের ওপর লেখা।

আর একটি মানবিক দিকও আছে: বিশাল সংখ্যক প্রতিষ্ঠান অল্প কয়েকজন স্বেচ্ছাসেবীর কাজের ওপর নির্ভর করে, প্রায়ই কোনো সহায়তা ছাড়া। Open-source-এর টেকসই রক্ষণাবেক্ষণ নিয়ে আলোচনা এই ঘটনার পরে আরও জোরালো হয়। নিরাপত্তার দিকটি Cybersecurity কোর্সে বিস্তারিত।

মূল উপলব্ধি: Test-এর টুলও আপনার software-এর অংশ। Version আটকে রাখুন, lock file commit করুন, আর এমন test লিখুন যা ডেটার নির্দিষ্ট মানে নয়, নিয়মে নির্ভর করে।

প্র ০৪Flaky test-এর তাৎক্ষণিক উপায় হিসেবে retry বনাম quarantine বনাম মুছে ফেলা, কোনটি কখন? Martin Fowler-এর “non-determinism” প্রবন্ধ এখানে কী বলে?

২০১১ সালে Martin Fowler “Eradicating Non-Determinism in Tests” নামে একটি প্রবন্ধ লেখেন। তাঁর মূল যুক্তি: অনির্ধারিত test সংক্রামক। একটি flaky test থাকলে মানুষ লাল দেখে উপেক্ষা করতে শেখে, তখন বাকি নির্ভরযোগ্য test-গুলোর সংকেতও হারায়। তিনি quarantine-এর প্রস্তাব দেন: flaky test-কে মূল pipeline থেকে আলাদা জায়গায় সরানো, যাতে মূল suite আবার বিশ্বাসযোগ্য হয়, আর quarantine-এর আকারে সীমা ও সময়সীমা রাখা, যাতে সেটি আবর্জনার স্তূপ না হয়। প্রবন্ধে এই পর্বের অনেক কারণও আলোচিত: async, বাইরের সেবা, সময়, ভাগ করা অবস্থা।

Retry সবচেয়ে সহজ, আর সবচেয়ে বিপজ্জনক। এই পর্বের ল্যাবে দেখলেন, --retry=2 দেওয়া test terminal-এ সবুজ, অথচ JSON report-এ failure message লুকানো। Retry যদি সব test-এ ডিফল্ট হয়, তাহলে সত্যিকারের race বা timing বাগ, যা ব্যবহারকারী মাঝে মাঝে দেখবেন, সেটি test-এ কখনো লাল হয় না। Retry-এর একটি সৎ জায়গা আছে: E2E-তে সত্যিকারের নিয়ন্ত্রণ-বহির্ভূত ক্ষণিক সমস্যা, আর তখনও “retry-তে পাস” আলাদা গুনে নিয়মিত দেখা।

Quarantine মাঝামাঝি পথ: test হারায় না, কিন্তু অন্যদের আটকায় না। এর সফলতা নির্ভর করে শৃঙ্খলায়: প্রতিটির issue, মালিক আর সময়সীমা। নইলে কয়েক মাসে quarantine-এ পঞ্চাশটি test জমে, কেউ দেখে না, আর সেগুলো যে আচরণ পাহারা দিত, তা পাহারাহীন।

মুছে ফেলাও কখনো কখনো সঠিক। যে test এমন আচরণ যাচাই করে যা অন্য একটি নির্ভরযোগ্য test-ও করে, বা যার প্রশ্নটি আর প্রাসঙ্গিক নয়, তাকে সারানোর পেছনে দিন ব্যয় করার চেয়ে মুছে ফেলা ভালো, তবে সিদ্ধান্তটি স্পষ্ট লিখে।

মূল উপলব্ধি: Retry লুকায়, quarantine সময় কেনে, সারানো সমস্যা মেটায়। ভালো দল সময় কেনে শুধু সারানোর জন্য, আর মাপে কতগুলো test সেখানে কতদিন আছে।

প্র ০৫MSW-এর মতো network স্তরে নকল আর পুরোনো ধাঁচে fetch বা axios module-কে সরাসরি mock করা, দুটির পার্থক্য কী? কেন অনেক দল প্রথমটিতে সরে গেছে?

অনেক বছর ধরে JavaScript-এ API নকল করার প্রচলিত উপায় ছিল module mock: jest.mock('axios') লিখে axios.get-কে একটি তৈরি উত্তর ফেরত দিতে বলা, বা global.fetch-কে একটি vi.fn() দিয়ে বদলে দেওয়া। এতে কাজ চলে, কিন্তু কয়েকটি দুর্বলতা আছে। নকলটি আপনার কোড কোন library দিয়ে অনুরোধ পাঠায়, তার সাথে বাঁধা; axios থেকে fetch-এ গেলে সব test নতুন করে লিখতে হয়। আপনার HTTP client-এর আসল কাজ, যেমন URL বানানো, header যোগ, JSON parse, status দেখে error ছোড়া, সেগুলো test-এ চলে না, কারণ ঠিক সেই ফাংশনটিই নকল। আর browser-এ dev বা Storybook-এ একই নকল ব্যবহার করা যায় না।

MSW (প্রথম প্রকাশ ২০১০-এর দশকের শেষ দিকে) উল্টো পথ নেয়। আপনার কোড অপরিবর্তিত, library যা-ই হোক; নকল বসে অনুরোধ যখন process বা browser থেকে বেরোতে যায়। Browser-এ এটি service worker API ব্যবহার করে, যা মূলত offline web app-এর জন্য তৈরি হয়েছিল; সেখানে fetch event ধরে নিজের উত্তর দেওয়ার ক্ষমতা আছে। Node-এ আলাদা interceptor library আছে, যা HTTP আর fetch-এর স্তরে কাজ করে। ফলে আপনার URL, header আর body, সব আসলে তৈরি হয়ে handler-এ পৌঁছায়, আর handler চাইলে সেগুলো যাচাই করতে পারে।

দাম কী? একটি অতিরিক্ত স্তর, আর version বদলের সাথে মানিয়ে নেওয়া; এই পর্বে MSW 2 থেকে 3-এ import আর option-এর নাম বদল দেখলেন, যা অসতর্ক থাকলে নীরবে সুরক্ষা সরিয়ে দেয়। আর একটি ধারণাগত সীমা: MSW-ও শেষ পর্যন্ত আপনার লেখা নকল, তাই আসল সেবা বদলালে তা পুরোনো হয়। এজন্য contract test বা schema-যাচাই লাগে।

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

অনুশীলন

হিসাব, কমান্ড, সমালোচনা আর ফল অনুমান, চারটিই আছে।

  1. Flake-এর সম্ভাবনা (numerical): (ক) একটি test প্রতি run-এ স্বাধীনভাবে ২% সম্ভাবনায় ফেল করে। ৫০ বার চালালে অন্তত একবার ফেল দেখার সম্ভাবনা কত? (খ) ১% flaky একটি test-কে ৯৫% নিশ্চয়তায় অন্তত একবার ফেল করতে দেখতে কমপক্ষে কত বার চালাতে হবে? (গ) ২০০টি test-এর suite-এ প্রতিটি ০.৫% flaky হলে একটি run পুরো সবুজ হওয়ার সম্ভাবনা কত, আর ১০০টি run-এ গড়ে কতটি লাল?

    (ক) 1 − 0.9850 ≈ 1 − 0.364 = ০.৬৩৬, প্রায় ৬৪%. অর্থাৎ ৫০ বার চালিয়েও প্রায় এক-তৃতীয়াংশ সম্ভাবনায় এটি ধরা পড়বে না। (খ) 1 − 0.99n ≥ 0.95, তাই n ≥ ln(0.05) / ln(0.99) ≈ ২৯৮.০৭, অর্থাৎ ২৯৯ বার (২৯৮-এ সম্ভাবনা ০.৯৪৯৯৬, সামান্য কম)। (গ) 0.995200 ≈ ০.৩৬৭; ১০০টি run-এ গড়ে প্রায় ৬৩টি লাল, যদিও কোনো test-ই “বেশিরভাগ সময় ঠিক” থেকে খারাপ নয়।

  2. কমান্ড দিয়ে খোঁজা: একটি Vitest প্রজেক্টে tests/cart.test.ts CI-তে মাঝে মাঝে ফেল করে, আপনার মেশিনে কখনো নয়। চারটি সম্ভাব্য কারণ (সময়, ক্রম, এলোমেলো ডেটা, সমান্তরাল database) প্রতিটি আলাদা করে যাচাইয়ের কমান্ড লিখুন, আর বলুন কোন ফল দেখলে কারণটি নিশ্চিত।
    • সময়: TZ=UTC npx vitest run tests/cart.test.ts আর TZ=Asia/Dhaka …; একটিতে লাল, অন্যটিতে সবুজ হলে time zone.
    • ক্রম: … --sequence.shuffle --sequence.seed=1, তারপর 2, 3, …; কোনো seed-এ লাল আর ফাইলের ক্রমে সবুজ হলে ক্রম-নির্ভরতা। লাল seed-টি লিখে রাখুন, সেটিতে ফল প্রতিবার একই।
    • এলোমেলো ডেটা: … --repeats=49; ক্রম একই রেখে কিছু repeat লাল হলে এলোমেলো মান বা সময়, বার্তার মান দেখে কোনটি।
    • সমান্তরাল database: পুরো suite npx vitest run --repeats=49 বনাম --no-file-parallelism. প্রথমটিতে লাল, দ্বিতীয়টিতে সবুজ হলে অন্য ফাইল একই সারি বা টেবিল ছুঁয়েছে।
  3. Test-এর সমালোচনা (নকশা): অন্তত ছয়টি সমস্যা ধরুন আর সমাধান বলুন: import fixtures from './orders.json'; vi.mock('../src/db'); test('order', async () => { const o = fixtures[2]; o.status = 'PAID'; o.createdAt = new Date().toLocaleDateString(); o.qty = faker.number.int(10); db.order.create.mockResolvedValue(o); placeOrder(o); await fetch('https://sms.example.com/send', { method: 'POST' }); await new Promise(r => setTimeout(r, 2000)); expect(db.order.create).toHaveBeenCalled(); });
    • ভাগ করা fixture বদলানো: অন্য test-এর ডেটা বদলায়। সমাধান: factory.
    • নিজের database mock: SQL চলে না। সমাধান: test database (পর্ব ৭৫)।
    • new Date().toLocaleDateString(): সময় আর time zone-নির্ভর। সমাধান: vi.setSystemTime, zone স্পষ্ট।
    • faker.number.int(10) seed ছাড়া, আর এতে 0-ও আসতে পারে। সমাধান: seed আর স্পষ্ট মান।
    • placeOrder(o)-এ await নেই: assertion কাজ শেষ হওয়ার আগে চলতে পারে।
    • আসল SMS ঠিকানায় fetch: network leak, খরচ, আর আসল মানুষের ফোন। সমাধান: MSW handler, onUnhandledFrame: 'error'.
    • setTimeout(…, 2000): ধীর আর অনির্ভরযোগ্য অপেক্ষা। সমাধান: await বা নকল ঘড়ি।
    • শুধু toHaveBeenCalled(): ফলাফল যাচাই নেই। সমাধান: database-এর সারি বা ফেরত মান দেখা।
  4. ফল অনুমান: একটি MSW 3 setup-এ মূল handler-এর তালিকায় GET */api/stock উত্তর দেয় {"qty": 10}. afterEach-এ server.resetHandlers(), আর listen onUnhandledFrame: 'error' দিয়ে। Test ১-এ প্রথমে server.use(GET */api/stock → {"qty": 0}), তারপর দুবার fetch('/api/stock'), তারপর আবার server.use(GET */api/stock → 500) আর একবার fetch. Test ২-এ কোনো use নেই, একবার /api/stock আর একবার /api/price. পাঁচটি fetch-এর প্রতিটির ফল কী?

    Test ১: প্রথম fetch {"qty": 0}, দ্বিতীয়টিও {"qty": 0}, কারণ use-এর handler সাধারণভাবে একবার ব্যবহারে মুছে যায় না (একবারের জন্য আলাদা once option লাগে)। তৃতীয় fetch 500, কারণ পরে যোগ করা handler তালিকার সবার সামনে বসে। Test ২: resetHandlers আগের test-এর সব use সরিয়ে দিয়েছে, তাই /api/stock → {"qty": 10}; /api/price-এর কোনো handler নেই, তাই অনুরোধ network-এ যায় না, fetch ব্যর্থ হয় (“fetch failed”), আর MSW-এর error বার্তা ছাপা হয়।

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

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

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

  1. Test-টি অনেকবার চালিয়ে ফেলের হার মাপা, যেমন ৫০ বারে ৪ বার
  2. ফেলের বার্তা আর ডেটা পড়ে সম্ভাব্য কারণ চিহ্নিত করা
  3. কারণটি নির্দিষ্টভাবে ঘটিয়ে প্রতিবার ফেল করানো, যেমন নির্দিষ্ট seed বা TZ দিয়ে
  4. কোড বা test-এর মূল কারণ সারানো
  5. আবার অনেকবার চালিয়ে শূন্য ফেল নিশ্চিত করা

একটি অনলাইন দোকানের order service-এর test-এ কোন নির্ভরতাটি নকল না করে আসল রাখা সবচেয়ে যুক্তিসংগত?

  • SMS gateway-র API
  • Payment gateway
  • দোকানের নিজের PostgreSQL test database
  • সিস্টেমের ঘড়ি (Date.now)

MSW 3 ব্যবহার করা একটি প্রজেক্টের test setup-এ লেখা আছে server.listen({ onUnhandledRequest: 'error' }). একটি test এমন URL-এ fetch করে, যার কোনো handler নেই। কী ঘটবে?

  • MSW স্পষ্ট error দিয়ে অনুরোধ থামাবে, network-এ যাবে না
  • শুধু একটি warning ছাপা হবে, আর অনুরোধ সত্যিকারের network-এ যাবে
  • Test শুরুর আগেই server.listen error দিয়ে থেমে যাবে
  • MSW একটি খালি 200 উত্তর ফেরত দেবে

একটি test-এ cart-এর মোট হিসাবের জন্য seed ছাড়া এলোমেলো দাম ব্যবহার হয়। ৫০ বার চালালে ৪ বার ফেল করে, আর ফেলের বার্তায় দেখা যায় মোট ৳১০০০ পেরোলেই প্রত্যাশা ভুল হয়। সবচেয়ে ভালো সমাধান কোনটি?

  • Test-এ retry: 3 যোগ করা
  • দামের সর্বোচ্চ সীমা কমিয়ে দেওয়া, যাতে ছাড় কখনো না লাগে
  • শুধু faker.seed(1) যোগ করা, যাতে প্রতিবার একই মান আসে
  • সীমার ঠিক নিচে আর ঠিক ওপরে স্পষ্ট মান দিয়ে দুটি test, আর বাকি এলোমেলো ডেটায় seed

একটি Vitest ফাইলে দুটি test একই module-level array ব্যবহার করে। ফাইলের ক্রমে দুটিই পাস করে, কিন্তু --sequence.shuffle --sequence.seed=3 দিলে একটি ফেল করে। এর কারণ কী?

  • Test-গুলো ভাগ করা অবস্থার মাধ্যমে একে অপরের ওপর নির্ভরশীল
  • Vitest-এর shuffle বাগযুক্ত
  • Seed 3 test-এর ভেতরের এলোমেলো ডেটা বদলে দিয়েছে
  • দুটি test আলাদা worker-এ একসাথে চলছে

একটি toast “বন্ধ” চাপলে ৯৮০ ms পরে DOM থেকে সরে। Test আসল ঘড়িতে ১০০০ ms সীমার মধ্যে উধাও হওয়ার অপেক্ষা করে, আর ব্যস্ত CI মেশিনে মাঝে মাঝে timeout হয়। সবচেয়ে নির্ভরযোগ্য সমাধান কোনটি?

  • অপেক্ষার সীমা ৫০০০ ms করা
  • Click-এর পরে ১ সেকেন্ড sleep
  • vi.useFakeTimers() দিয়ে vi.advanceTimersByTime(980)
  • Test-এ retry যোগ করা

একটি frontend আর backend দুটি আলাদা দল বানায়। Frontend-এর MSW handler-এ পণ্যের দাম pricePaisa, কিন্তু backend সম্প্রতি সেটি বদলে price করেছে। কোন ব্যবস্থা deploy-এর আগে এই অমিল সবচেয়ে নির্ভরযোগ্যভাবে ধরে?

  • Frontend-এর component test আরও বাড়ানো
  • Consumer-driven contract test, যা provider-এর CI-তে চুক্তি যাচাই করে
  • MSW handler-এর উত্তরের snapshot test
  • CI-তে সব test-এ retry চালু করা

২০০টি test-এর একটি suite-এ প্রতিটি test প্রতি run-এ স্বাধীনভাবে ০.৫% সম্ভাবনায় flaky ফেল করে। একটি run পুরো সবুজ হওয়ার সম্ভাবনা প্রায় কত?

  • ৯৯.৫%
  • ০%
  • ৯০%
  • প্রায় ৩৭%

শব্দকোষ

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

মনে রাখুন

  1. নকল শুধু যা আপনার নয়, ধীর বা অনিশ্চিত। নিজের database আর repository আসল; over-mocking সবুজ test-এর আড়ালে ভাঙা SQL লুকায়।
  2. MSW network-এর সীমানায়। একই handler Vitest-এ setupServer, browser-এ setupWorker; MSW 3-এ onUnhandledFrame: 'error', আর server.use দিয়ে error ও ধীর উত্তর।
  3. Factory আর override. প্রতিটি test নিজের ন্যূনতম ডেটা বানায়; seed দেওয়া faker, বাংলা নাম নিজের তালিকা থেকে, ভাগ করা fixture নয়।
  4. Flaky-র সাত কারণ: সময় ও zone, seed ছাড়া ডেটা, ক্রম ও ভাগ করা অবস্থা, await আর race, network leak, animation, একই DB সারি।
  5. মাপুন, সারান, লুকাবেন না। --repeats আর shuffle দিয়ে হার বের করুন, নকল ঘড়ি দিয়ে সময় স্থির করুন, quarantine সময়সীমাসহ, retry নয়; contract test mock-কে সৎ রাখে।

পরের পর্বে: মডিউল ১২ শুরু, বাজারঘর এবার production-এর পথে। প্রথম ধাপ environment আর secret: dev, staging আর production আলাদা রাখা, config যাচাই, আর secret কোডে বা log-এ না রাখা।

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

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