এই পর্বে যা শিখবেন
- ফুল-স্ট্যাক অ্যাপের চার স্তরের test কী চালায়, কী নকল রাখে, আর কী ধরতে পারে না
- Pyramid, trophy আর honeycomb-এর আসল trade-off: গতি, আস্থা আর রক্ষণাবেক্ষণের দাম
- “বাজারঘর”-এর জন্য ঝুঁকিভিত্তিক মানচিত্র: কোন feature কোন স্তরে, কোনটি test নয়
- নিজের ল্যাবে Vitest, Testing Library, supertest আর Playwright-এর সত্যিকারের run আর সময়
- Vitest-এর v8 coverage, Stryker দিয়ে mutation testing, AAA আকার আর ছোট দলের test budget
১ · Test কেন — refactor-এর সাহস আর deploy-এর ভরসা
বৃহস্পতিবার রাত। কাল থেকে বাজারঘরে ঈদের অফার শুরু। নতুন কুপন EID10: ১০% ছাড়, সর্বোচ্চ ২০০ টাকা। ডেভেলপার রাফি দামের হিসাবের function-এ তিন লাইন বদলালেন। এখন প্রশ্ন দুটি। এক, আগের নিয়মগুলো কি এখনো ঠিক আছে? ১০০০ টাকা বা তার বেশি কেনাকাটায় ফ্রি ডেলিভারি, স্টক না থাকলে অর্ডার বন্ধ, অন্যের অর্ডার দেখা যায় না। দুই, এই কোড কি রাতেই production-এ পাঠানো যায়?
Test ছাড়া দুটি প্রশ্নের উত্তর একটাই: “মনে হয় ঠিক আছে।” হাতে ক্লিক করে পাঁচটি পাতা দেখা যায়, পঞ্চাশটি নিয়ম দেখা যায় না। স্বয়ংক্রিয় test এখানে দুটি জিনিস দেয়।
- Refactor-এর সাহস। Refactoring মানে বাইরের আচরণ না বদলে ভেতরের গঠন বদলানো। কিন্তু আচরণ সত্যিই বদলায়নি, তা জানবেন কীভাবে? পর্ব ২৯-এ service-কে নকল repository দিয়ে test করেছিলেন, যাতে database বদলালেও নিয়মগুলো পাহারায় থাকে। ভালো test থাকলে কোড পরিষ্কার করা ভয়ের কাজ থাকে না।
- Deploy-এর ভরসা। প্রতিটি বদলের পরে একই পরীক্ষা যন্ত্র নিজে চালায়, মানুষ ক্লান্ত হলেও। পুরোনো ঠিক জিনিস নতুন বদলে ভেঙে যাওয়াকে বলে regression. Regression ধরাই test-এর প্রতিদিনের কাজ। এই test-গুলো প্রতিটি push-এ চালানোর pipeline আসবে পর্ব ৮১-এ।
গার্মেন্টস কারখানার মান যাচাই ভাবুন। সেলাই লাইনে ইনলাইন QC প্রতিটি টুকরোর সেলাই দেখেন: দ্রুত, সস্তা, হাজার বার। প্যাকিংয়ের আগে ফাইনাল ইন্সপেকশন পুরো শার্ট গায়ে দেওয়ার মতো করে দেখে: ধীর, দামি, কয়েকটি নমুনায়। শুধু ইনলাইন QC থাকলে বোতাম ঠিক কিন্তু মাপ ভুল শার্ট বেরিয়ে যায়। শুধু ফাইনাল ইন্সপেকশন থাকলে প্রতিটি ভুল ধরা পড়ে শেষ মুহূর্তে, আর কোন লাইনে ভুল হলো তা খুঁজতে দিন যায়। Test কৌশল মানে এই দুইয়ের মিশ্রণ ঠিক করা।
একটি সীমাও শুরুতেই মনে রাখুন। Edsger Dijkstra-র বহুল উদ্ধৃত কথা (১৯৬৯-৭০ সালের দিকের লেখা ও বক্তৃতায়): test বাগের উপস্থিতি দেখাতে পারে, অনুপস্থিতি কখনো নয়। সবুজ test মানে “যা যাচাই করেছি, তাতে ভুল পাইনি।” কী যাচাই করবেন, সেটি বাছাই করাই এই পর্বের বিষয়। Test-এর মৌলিক তত্ত্ব আছে Software Engineering কোর্সের testing fundamentals পর্বে; এখানে সেটি আবার পড়াব না।
বাজারঘরের একটি অনুরোধ কয়েকটি অংশ পার হয়: browser, React component, Express API, database, আর মাঝখানে দামের হিসাবের মতো খাঁটি logic. প্রতিটি ধরনের test এদের কোনটি আসলে চালায়, নিচের ছকে দেখুন। ছকের সময়গুলো আমার ল্যাবের একটি local run, সাধারণ ফল নয়।
কোন test কোন অংশ আসলে চালায়
Filled dot = the real thing runs; dashed ring = a stand-in (jsdom)
২ · ফুল-স্ট্যাকের চার স্তর — কোন test আসলে কী চালায়
নাম নিয়ে বিভ্রান্তি খুব বেশি। এক দলের “integration test” আরেক দলের “API test”. তাই নামের চেয়ে প্রশ্নটি জরুরি: এই test-এ কোন অংশ আসল, কোন অংশ নকল? বাজারঘরে চারটি স্তর এভাবে ভাগ করব, আর তার নিচে একটি পঞ্চম ভিত্তি।
| স্তর | বাজারঘরে উদাহরণ | Tool (লেখার সময়ে) | যা আসল | যা ধরতে পারে না |
|---|---|---|---|---|
| Static | pricePaisa-কে string ভেবে যোগ করা | TypeScript, ESLint | কোড, না চালিয়েই | যুক্তির ভুল |
| Unit | calcTotal-এর ছাড় আর ডেলিভারি | Vitest 5.1.0 | শুধু একটি function | অংশগুলো জোড়ার ভুল |
| Component | কুপন লিখলে মোট বদলায় | Testing Library 16.3.3 + jsdom 30.1.3 | React, আপনার component | আসল browser-এর layout, CSS |
| API + DB | স্টক না থাকলে 409, কিছুই বদলায় না | supertest 7.3.1, Express 5.3.0 | Route, middleware, SQL, transaction | Frontend আর browser |
| E2E | পণ্য যোগ থেকে অর্ডার নিশ্চিত | Playwright 1.64.1 | প্রায় সব | কোথায় ভাঙল, তা সহজে বলে না |
মাঝের দুটি স্তরকে অনেকে একসাথে integration test বলেন: কয়েকটি আসল অংশ একসাথে চালানো test. Static স্তরে test লিখতে হয় না, কিন্তু সে বিনা খরচে একদল ভুল আগেই ধরে। এর ব্যবস্থা পর্ব ৫-এ (ESLint) আর পর্ব ৬-এ (TypeScript) দেখেছেন। একে বলে static analysis.
এখন দেখা যাক একটি test runner আসলে কী করে। Vitest এই পাতায় চলে না, তাই নিচে ২০ লাইনের একটি শেখার মডেল। এটি Vitest নয়, শুধু তার মূল ধারণা: test একটি নাম আর একটি function নেয়, চালায়, আর ব্যতিক্রম ছুড়লে ফেল ধরে। expect একটি assertion বানায়।
// Teaching model of a test runner. Not Vitest, only the idea.
const results = [];
function test(name, fn) {
try { fn(); results.push(['PASS', name]); }
catch (e) { results.push(['FAIL', name + ' -> ' + e.message]); }
}
function expect(actual) {
return { toBe(want) {
if (!Object.is(actual, want)) throw new Error(`expected ${want}, got ${actual}`);
} };
}
// Code under test: paisa in, paisa out (100 paisa = 1 taka).
function calcTotal(items, coupon) {
const subtotal = items.reduce((s, it) => s + it.pricePaisa * it.qty, 0);
const discount = coupon === 'EID10' ? Math.min(Math.round(subtotal * 0.1), 20000) : 0;
const delivery = subtotal - discount >= 100000 ? 0 : 6000;
return subtotal - discount + delivery;
}
const rice = { pricePaisa: 45000, qty: 2 };
const dal = { pricePaisa: 15000, qty: 1 };
test('below 1000 taka pays 60 taka delivery', () => expect(calcTotal([dal])).toBe(21000));
test('1050 taka cart has free delivery', () => expect(calcTotal([rice, dal])).toBe(105000));
test('EID10 can bring delivery back', () => expect(calcTotal([rice, dal], 'EID10')).toBe(100500));
test('discount is capped at 200 taka', () =>
expect(calcTotal([{ pricePaisa: 500000, qty: 1 }], 'EID10')).toBe(480000));
test('a wrong expectation fails loudly', () => expect(calcTotal([dal])).toBe(15000));
for (const [status, name] of results) console.log(status, name);
const failed = results.filter((r) => r[0] === 'FAIL').length;
console.log(`Tests: ${results.length - failed} passed, ${failed} failed`);প্রথম চারটি লাইন PASS, পঞ্চমটি ইচ্ছা করে ভুল: FAIL a wrong expectation fails loudly -> expected 15000, got 21000. শেষে Tests: 4 passed, 1 failed. তৃতীয় test-টি লক্ষ করুন। ১০৫০ টাকার কার্টে কুপন দিলে ১০৫ টাকা ছাড়, মোট নামে ৯৪৫-এ, তাই ডেলিভারি আবার ফিরে আসে: ৯৪৫ + ৬০ = 100500 পয়সা, অর্থাৎ ১০০৫ টাকা। ক্রেতা অবাক হতে পারেন, তবে নিয়ম এটাই, আর test সেটি লিখে রাখল। ফেল হলে বার্তাটি “কী চেয়েছিলাম, কী পেলাম” দুটোই বলে; ভালো assertion-এর এটিই গুণ।
দামগুলো পয়সায়, পূর্ণসংখ্যায়, যেমন পর্ব ৪৩-এ দেখেছেন। Vitest-এর describe, it, expect একই আকারের, শুধু অনেক বেশি সুবিধাসহ: watch mode, mock, snapshot, coverage. সেগুলো আসবে পর্ব ৭৩-এ।
৩ · Pyramid, trophy আর honeycomb — একই প্রশ্নের তিন উত্তর
কোন স্তরে কতগুলো test রাখবেন, তার তিনটি পরিচিত ছবি আছে। বিস্তারিত ইতিহাস আর ছবি আছে test pyramid ও integration পর্বের অংশ ৩-এ. এখানে সংক্ষেপে, তারপর ফুল-স্ট্যাকের চোখে।
Test pyramid
Test pyramid সাধারণত Mike Cohn-এর নামে বলা হয়; তাঁর Succeeding with Agile (২০০৯) বই ছবিটি জনপ্রিয় করে। নিচে অনেক unit test, মাঝে কম service test, ওপরে অল্প UI test. যুক্তি: নিচের test দ্রুত আর সস্তা।
হিসাবের নিয়মে ভরা কোডTesting trophy
Testing trophy Kent C. Dodds-এর প্রস্তাব, আনুমানিক ২০১৮ সালে, JavaScript frontend মাথায় রেখে। ভিত্তিতে static check, তারপর unit, সবচেয়ে মোটা অংশ integration, ওপরে অল্প E2E.
Component-এ ভরা UITest honeycomb
Honeycomb Spotify-এর engineering blog-এ (আনুমানিক ২০১৮) microservice-এর জন্য বর্ণিত। ছোট service-এ ভেতরের যুক্তি কম, ঝুঁকি সীমানায়: database আর অন্য service. তাই বেশিরভাগ test service-কে তার API দিয়ে চালায়।
Database-এ কথা বলা APIতিনটি ছবি তিনটি মাপের মধ্যে টানাপোড়েন দেখায়। কোনো স্তর তিনটিতেই জেতে না।
| স্তর | গতি | আস্থা | রক্ষণাবেক্ষণের দাম | ফেল করলে কোথায় খুঁজবেন |
|---|---|---|---|---|
| Unit | সবচেয়ে দ্রুত | এক function-এর নিয়মে উচ্চ, পুরো অ্যাপে কম | কম, যদি আচরণ ধরে লেখা হয় | প্রায় নির্দিষ্ট লাইনে |
| Component | দ্রুত | UI-র আচরণে ভালো | মাঝারি; markup ধরে লিখলে বেশি | এক component-এ |
| API + DB | মাঝারি | Backend-এ উচ্চ: SQL, transaction, অনুমতি | মাঝারি; test ডেটা সামলাতে হয় | এক route-এর পথে |
| E2E | সবচেয়ে ধীর | সবচেয়ে বাস্তব | বেশি; সময়, নেটওয়ার্ক, অস্থিরতা | পুরো stack-এর যেকোনো জায়গায় |
হিসাব করে দেখুন: suite কত সময় নেয়
একটি ধরে নেওয়া suite: ৩০০টি unit test, প্রতিটি গড়ে ২ ms; ৮০টি component test, প্রতিটি ৩০ ms; ৬০টি API test, প্রতিটি ১০ ms; আর ১০টি E2E, প্রতিটি ৩ সেকেন্ড। সংখ্যাগুলো উদাহরণ, মাপা নয়।
- Unit: ৩০০ × ২ = ৬০০ ms
- Component: ৮০ × ৩০ = ২,৪০০ ms
- API: ৬০ × ১০ = ৬০০ ms
- E2E: ১০ × ৩,০০০ = ৩০,০০০ ms
- মোট: ৩৩,৬০০ ms, অর্থাৎ ৩৩.৬ সেকেন্ড। E2E-র ভাগ ৩০,০০০ ÷ ৩৩,৬০০ ≈ ৮৯.৩%, অথচ সংখ্যায় সেগুলো মোট test-এর মাত্র ২%.
E2E চারটি worker-এ ভাগ করে চালালে তাদের অংশ হয় ৭.৫ সেকেন্ড, আর মোট ৩.৬ + ৭.৫ = ১১.১ সেকেন্ড. শিক্ষা দুটি। এক, suite-এর সময় নির্ধারণ করে ধীরতম স্তরের সংখ্যা, মোট test-এর সংখ্যা নয়। দুই, প্রতিটি নতুন E2E test ৩,০০০ ÷ ২ = ১,৫০০টি unit test-এর সমান সময় খায়। তাই E2E-তে শুধু সেই পথ রাখুন, যা নিচের স্তরে কোনোভাবেই ধরা যায় না।
বাজারঘরের জন্য কোন আকার
সৎ উত্তর: একটি আকার নয়। ফুল-স্ট্যাক অ্যাপ আসলে তিন রকম কোডের মিশ্রণ, আর প্রতিটি অংশ নিজের আকার চায়।
- দাম, ছাড়, VAT, স্টক গোনা: খাঁটি নিয়ম, অনেক কিনারা। এখানে pyramid-এর মতো অনেক unit test.
- React UI: খুঁটিনাটির unit test refactor-এ ভাঙে। এখানে trophy-র মতো component test, যা কয়েকটি component একসাথে চালায়।
- Express API আর database: ভুল থাকে SQL, transaction আর অনুমতিতে। এখানে honeycomb-এর মতো API test, আসল database-সহ।
- সব মিলিয়ে: অল্প কয়েকটি E2E, শুধু টাকা আর অর্ডারের প্রধান পথে।
সম্প্রদায়ে এ নিয়ে বিতর্ক আছে, আর সেটি স্বাভাবিক। Pyramid-এর সমর্থকেরা বলেন, integration-ভারী suite ধীর হয় আর ফেলের কারণ খুঁজতে সময় লাগে। Trophy-র সমর্থকেরা বলেন, ছোট ছোট unit test একসাথে কাজ করার ভুল ধরে না। দুই পক্ষই ঠিক, ভিন্ন ধরনের কোডের জন্য। Trophy-র “integration” প্রায়ই browser বা network ছাড়াই চলে, তাই আসলে দ্রুত। নাম না দেখে প্রতিটি test-এর গতি আর স্থিরতা দেখুন।
৪ · বাজারঘরে কী test করবেন — ঝুঁকির মানচিত্র
সময় সীমিত, তাই সব কিছুর সমান test হবে না। প্রশ্নটি হলো: কোথায় ভুল হলে সবচেয়ে বেশি ক্ষতি, আর কোথায় ভুল হওয়ার সম্ভাবনা বেশি? এই দুটি গুণ করলে পাওয়া যায় একটি ঝুঁকির নম্বর। এই পদ্ধতিকে বলে risk-based testing. নিচের সংখ্যাগুলো একটি দলের অনুমান, মাপা তথ্য নয়; আপনার দল নিজের সংখ্যা বসাবে।
// Risk = likelihood (1-5) x impact (1-5). The numbers are a team's guess.
const features = [
['Order total and coupon', 4, 5],
['Stock check on checkout', 3, 5],
['Payment webhook', 3, 5],
['Seller sees only own orders', 2, 5],
['Login and password reset', 2, 4],
['Product search filters', 3, 2],
['Bangla date on invoice', 2, 2],
['Footer link colours', 2, 1],
];
function layerFor(score) {
if (score >= 15) return 'many unit + API tests, one E2E path';
if (score >= 8) return 'API or component tests';
if (score >= 4) return 'one test at the cheapest layer';
return 'no automated test, look at it';
}
const rows = features
.map(([name, likely, impact]) => ({ name, likely, impact, score: likely * impact }))
.sort((a, b) => b.score - a.score);
console.log('feature'.padEnd(28), 'L', 'I', 'score', ' suggested');
let all = 0;
for (const r of rows) {
console.log(r.name.padEnd(28), r.likely, r.impact, String(r.score).padStart(5), '', layerFor(r.score));
all += r.score;
}
const top3 = rows.slice(0, 3).reduce((s, r) => s + r.score, 0);
console.log(`top 3 of ${rows.length} features carry ${Math.round((top3 / all) * 100)}% of the risk`);সারিগুলো ঝুঁকি অনুযায়ী সাজানো। ওপরে Order total and coupon, score 20; তারপর স্টক আর payment webhook, দুটিই 15. নিচে Footer link colours, score 2, পরামর্শ no automated test, look at it. শেষ লাইন: top 3 of 8 features carry 63% of the risk. মোট ঝুঁকি ৮০, তার ৫০ মাত্র তিনটি feature-এ; ৫০ ÷ ৮০ = ৬২.৫%, যা পূর্ণসংখ্যায় ৬৩. অর্থাৎ test-এর সময়ের বড় অংশ কোথায় যাবে, সেটি এক নজরে পরিষ্কার।
এবার মানচিত্রটি স্তরসহ। প্রতিটি সারিতে প্রশ্ন একটাই: এই ভুলটি সবচেয়ে সস্তায় কোন স্তরে ধরা যায়?
| Feature | কী ভাঙতে পারে | স্তর | আগে দেখেছেন |
|---|---|---|---|
| মোট দাম, কুপন, ডেলিভারি | সীমানায় ভুল, পয়সার গোলমাল | অনেক unit, একটি API | পর্ব ৪৩ |
| Checkout-এ স্টক | শেষ পণ্য দুজনের কাছে বিক্রি | API + DB, transaction-সহ | পর্ব ৪৩ |
| Payment webhook | একই ঘটনা দুবার, ভুয়া সই | API + DB, নকল gateway দিয়ে | পর্ব ৬৫ |
| Seller শুধু নিজের অর্ডার | IDOR, অন্যের তথ্য ফাঁস | API-তে role বনাম route ছক | পর্ব ৪৮ |
| Login, password reset | Session, rate limit | API, আর একটি E2E | পর্ব ৪৫, ৪৯ |
| পণ্য খোঁজা আর filter | ভুল ফল, ধীর query | API, query budget | পর্ব ৩৪, ৪২ |
| কার্টের পাতা | মোট না বদলানো, বোতাম ভুল সময়ে চালু | Component | পর্ব ১২, ২১ |
| পুরো কেনাকাটা | অংশগুলো জোড়ায় ভুল | ১ থেকে ৩টি E2E | পর্ব ৬৫ |
| বাংলা তারিখ, সংখ্যা | ভুল format | Unit | পর্ব ৫৯ |
| Footer-এর রং, pixel-এর মাপ | দেখতে খারাপ | স্বয়ংক্রিয় test নয়; চোখে দেখা, review | পর্ব ২২ |
কী test করবেন না, সেটিও সিদ্ধান্ত। React নিজে ঠিক কাজ করে কি না, Prisma সারি লেখে কি না, এসব তাদের নিজেদের দলের কাজ। Private helper-এর আলাদা test সাধারণত লাগে না; বাইরের আচরণের test সেগুলো এমনিই চালায়। বাইরের payment gateway-কে প্রতিটি test-এ সত্যি ডাকবেন না; পর্ব ৬৫-এর নকল gateway-ই যথেষ্ট। আর pixel-এর মাপ test-এ বাঁধলে প্রতিটি design বদলে test লাল হবে, অথচ ক্রেতার কোনো ক্ষতি হয়নি।
একটি feature হাতে এলে স্তর বাছাইয়ের চিন্তা ধাপে ধাপে দেখুন। প্রতিটি ধাপে একটি নতুন feature, আর সবচেয়ে সস্তা যে স্তর ভুলটি ধরতে পারে, সেটি উজ্জ্বল হবে।
৫০০০ টাকার কার্ট, কুপন EID10
১০% = ৫০০ টাকা → সীমা ২০০ টাকা
calcTotal(items, 'EID10').discount === 20000
ক্রেতা "eid10" লিখলেন → মোট ৳1,005
"অর্ডার করুন" চাপলেন
onOrder 100500 দিয়ে ডাকা হলো কি?
ডাল স্টকে ১টি, অর্ডারে চাল ১ + ডাল ২
প্রত্যাশা: 409, চালের স্টকও আগের মতো ১০
(প্রথম লাইন rollback হয়েছে)
পাতা খোলা → চাল কার্টে → অর্ডার করুন
পাতায় দেখা যায়: অর্ডার নিশ্চিত, মোট ৳510
body: { productId: 2, qty: 1, pricePaisa: 1 }
প্রত্যাশা: দাম database থেকে, মোট 21000
API পাঠায় pricePaisa: "45000" (string)
React-এ "45000" + 6000 → "450006000"
ভাগ করা type থাকলে compile-এই ধরা পড়ে
ঝুঁকি = ২ × ১ = ২
ভুল হলে: দেখতে খারাপ, ক্ষতি নেই
test লিখলে: প্রতিটি design বদলে লাল
- খাঁটি নিয়ম, unit. সীমার নিয়মটি শুধু সংখ্যা নিয়ে কাজ করে: কোনো DOM নেই, database নেই। তাই সবচেয়ে সস্তা স্তরেই একে বেঁধে ফেলুন, আর কিনারার মানগুলো আলাদা আলাদা test-এ দিন।
- ক্রেতার হাতের কাজ, component. টাইপ করা, বোতাম চাপা আর সেই চাপে সঠিক মোট পাঠানো: এর জন্য React আর DOM লাগে, কিন্তু server লাগে না। Testing Library ঠিক এখানে।
- Database-এর নিয়ম, API + DB. Rollback ঘটে database-এ। নকল repository দিয়ে এটি প্রমাণ হয় না, তাই আসল SQL চালানো API test দরকার।
- জোড়ার প্রমাণ, একটি E2E. HTML, JavaScript, API আর database একসাথে ঠিক আছে কি না, শুধু browser-ই বলতে পারে। এমন test অল্প রাখুন, প্রধান পথে।
- নিরাপত্তার নিয়ম, API. দাম client থেকে নেওয়া হয় কি না, তা বোঝা যায় শুধু server-এ অনুরোধ পাঠিয়ে। Frontend-এর test এখানে কিছুই প্রমাণ করে না, কারণ আক্রমণকারী frontend ব্যবহারই করেন না।
- আকৃতির ভুল, static. String আর number গুলিয়ে যাওয়া test লেখার আগেই TypeScript ধরতে পারে, যদি API আর React একই type ভাগ করে (পর্ব ৩৭-এর মতো)। এটি বিনা খরচের স্তর।
- Test নয়। ঝুঁকি খুব কম, অথচ test হবে ভঙ্গুর। এখানে চোখে দেখা আর code review যথেষ্ট। প্রতিটি জিনিসের test না লেখা অলসতা নয়, এটিও একটি সিদ্ধান্ত।
৫ · আচরণ test করুন, implementation-এর খুঁটিনাটি নয়
Testing Library-র মূল নীতিটি Kent C. Dodds এভাবে বলেন: test যত বেশি আপনার software-এর আসল ব্যবহারের মতো হবে, তত বেশি আস্থা দেবে। উল্টো দিকটি হলো implementation detail: class-এর নাম, ভেতরের state, কোন helper কতবার ডাকা হলো। ব্যবহারকারী এগুলো দেখেন না। এসব ধরে লেখা test হয় brittle test: আচরণ না বদলালেও ভাঙে।
ভঙ্গুর test-এর দুটি দোষ। এক, নির্দোষ refactor-এ লাল হয়ে মিথ্যা alarm দেয়। দুই, যা আসলে জরুরি তা যাচাই করে না, তাই আসল বাগ সবুজ রঙেই পার হয়ে যায়। নিচে একই কার্টের তিনটি বাস্তবায়ন: v1, তার নির্দোষ refactor v2, আর একটি সত্যিকারের বাগসহ v3.
// Three implementations of the same cart summary.
function summaryV1(items) {
const lines = items.map((it) => it.pricePaisa * it.qty);
const total = lines.reduce((a, b) => a + b, 0);
return { lines, total, text: 'Total: Tk ' + total / 100 };
}
function summaryV2(items) { // harmless refactor: no lines array any more
let total = 0;
for (const it of items) total += it.pricePaisa * it.qty;
return { total, text: 'Total: Tk ' + total / 100 };
}
function summaryV3(items) { // a real bug: the text shows only the first line
const lines = items.map((it) => it.pricePaisa * it.qty);
const total = lines.reduce((a, b) => a + b, 0);
return { lines, total, text: 'Total: Tk ' + lines[0] / 100 };
}
const cart = [{ pricePaisa: 45000, qty: 2 }, { pricePaisa: 15000, qty: 1 }];
// Brittle: reaches into an internal field that exists only by accident.
const brittle = (s) => s.lines.length === 2 && s.lines[0] === 90000;
// Behaviour: checks only what the shopper reads.
const behaviour = (s) => s.text === 'Total: Tk 1050';
for (const [name, impl] of [['v1', summaryV1], ['v2', summaryV2], ['v3', summaryV3]]) {
for (const [kind, check] of [['brittle', brittle], ['behaviour', behaviour]]) {
let verdict;
try { verdict = check(impl(cart)) ? 'PASS' : 'FAIL'; }
catch (e) { verdict = `FAIL (${e.constructor.name}: ${e.message})`; }
console.log(name, kind.padEnd(9), verdict);
}
}ছয় লাইনের ফলই পুরো শিক্ষা। v1-এ দুটিই PASS. v2-তে আচরণ একই, তবু v2 brittle FAIL (TypeError: Cannot read properties of undefined (reading 'length')), আর v2 behaviour PASS. v3-তে ক্রেতা ভুল মোট (৯০০ টাকা) দেখছেন, অথচ v3 brittle PASS; শুধু v3 behaviour FAIL বাগটি ধরল। ভঙ্গুর test দুবারই ভুল উত্তর দিল: একবার মিথ্যা লাল, একবার মিথ্যা সবুজ।
আসল React component-এ একই ঘটনা
এবার ল্যাবে আসল জিনিস: React 19.3.1-এর CartSummary, Vitest 5.1.0, Testing Library 16.3.3 আর jsdom 30.1.3. প্রথম test-টি markup চেনে:
// Brittle: it knows the class names and the order of the spans.
it('total cell shows 1,050', () => {
const { container } = render(<CartSummary items={items} onOrder={() => {}} />);
const cell = container.querySelector('.row.total span:last-child');
expect(cell.textContent).toBe('৳1,050');
expect(container.querySelectorAll('.row')).toHaveLength(2);
});দ্বিতীয় test ক্রেতার চোখে দেখে: পাতার লেখা, label দিয়ে খোঁজা input, আর role ও নাম দিয়ে খোঁজা বোতাম।
it('a coupon changes the total the order is placed with', async () => {
const user = userEvent.setup();
const onOrder = vi.fn();
render(<CartSummary items={items} onOrder={onOrder} />);
await user.type(screen.getByLabelText('কুপন'), 'eid10');
await user.click(screen.getByRole('button', { name: 'অর্ডার করুন' }));
expect(onOrder).toHaveBeenCalledWith(100500);
});তারপর একটি নির্দোষ refactor: div আর span-এর বদলে dl, dt, dd, আর পুরোনো class-এর বদলে Tailwind-এর class (পর্ব ২২)। ক্রেতার চোখে পাতা হুবহু একই। নিচের stepper-এ সত্যিকারের run ধরে এগিয়ে দেখুন।
<div className="row total">
<span>মোট</span><span>{formatTaka(t.total)}</span>
</div>
✓ tests/brittle/cart.brittle.test.jsx > total cell shows 1,050 32ms
✓ ... > shows the total and free delivery 24ms
✓ ... > a coupon changes the total the order is placed with 228ms
✓ ... > an empty cart cannot be ordered 4ms
Tests 4 passed (4)
<dl className="grid grid-cols-2 gap-1">
<dt className="font-bold">মোট</dt>
<dd className="font-bold">{formatTaka(t.total)}</dd>
</dl>
× tests/brittle/cart.brittle.test.jsx > total cell shows 1,050 32ms
→ Cannot read properties of null (reading 'textContent')
❯ tests/brittle/cart.brittle.test.jsx:13:15
13| expect(cell.textContent).toBe('৳1,050');
✓ ... > shows the total and free delivery 25ms
✓ ... > a coupon changes the total the order is placed with 200ms
✓ ... > an empty cart cannot be ordered 5ms
Tests 1 failed | 3 passed (4)
onClick={() => onOrder(t.subtotal)} // should be t.total
× a coupon changes the total the order is placed with 253ms
AssertionError: expected "vi.fn()" to be called with arguments: [ 100500 ]
- 100500,
+ 105000,
Test Files 1 failed | 1 passed (2)
- প্রথম রূপ। মোটের ঘরটি একটি
div, যার classrow total, ভেতরে দুটিspan. Brittle test ঠিক এই গঠন মুখস্থ করে রেখেছে। - সব সবুজ। v1-এ চারটি test-ই পাস। এই মুহূর্তে দুই ধরনের test আলাদা করা যায় না; পার্থক্য বোঝা যায় শুধু কোড বদলালে।
- Refactor. Markup
dl-এ গেল, class বদলে Tailwind হলো। মোট এখনো ৳1,050, ডেলিভারি এখনো ফ্রি, বোতাম একই কাজ করে। ক্রেতার কাছে কিছুই বদলায়নি। - মিথ্যা লাল।
.row.total span:last-childআর কিছু খুঁজে পায় না, তাইcellহলোnull, আর test ভাঙলTypeErrorদিয়ে। কোনো বাগ নেই, তবু CI লাল। কেউ test “ঠিক” করতে বসবেন, অর্থাৎ নতুন selector মুখস্থ করাবেন। - Behaviour test অটল। লেখা, label আর বোতামের নাম বদলায়নি, তাই তিনটিই পাস। ফল: ৪টির মধ্যে ১টি ফেল, আর সেটিই ভুল কারণে।
- আসল বাগ। এবার v1-এ সত্যিকারের ভুল ঢোকানো হলো: বোতাম ছাড়ের আগের দাম পাঠায়। Brittle test পাস করল, কারণ সে শুধু ঘরের লেখা দেখে। Behaviour test ধরল: চাওয়া
100500, পাওয়া105000. একজন ক্রেতা ৪৫ টাকা বেশি দিতেন (১০৫০ − ১০০৫)।
তাহলে implementation-এর test কি সবসময় নিষেধ? না। কিছু “ভেতরের” বিষয় আসলে চুক্তি। পর্ব ৪২-এর query budget test গোনে একটি পাতায় কতগুলো SQL চলল; এটি implementation, কিন্তু performance-এর চুক্তি, তাই ইচ্ছা করেই বাঁধা। নিয়ম হলো: যা ভাঙলে ব্যবহারকারী বা অন্য দল ক্ষতিগ্রস্ত হন, সেটি test করুন; যা বদলালে কারও কিছু যায় আসে না, সেটি test-এ বাঁধবেন না।
৬ · এক project, চার স্তর — সত্যিকারের run আর সময়
সব কথার প্রমাণ হিসেবে একটি ছোট project বানিয়েছি, যেখানে প্রতিটি স্তরে কয়েকটি test আছে। লেখার সময়ের version: Node.js 24.15.0, Vitest 5.1.0 আর @vitest/coverage-v8 5.1.0, React 19.3.1, @testing-library/react 16.3.3, @testing-library/user-event 14.6.8, @testing-library/jest-dom 7.0.1, jsdom 30.1.3, Express 5.3.0, supertest 7.3.1, @playwright/test 1.64.1 (Microsoft Edge-এ), আর Stryker 10.1.1. Database হিসেবে Node-এর নিজস্ব node:sqlite-এর in-memory database, যাতে প্রতিটি test নতুন, খালি database পায়। আসল PostgreSQL-এ test database বানানো আসবে পর্ব ৭৫-এ।
~/bazarghar
├── src/price.js calcTotal, formatTaka
├── src/CartSummary.jsx React component
├── src/app.js Express API + node:sqlite
├── public/index.html a tiny shop page for E2E
├── tests/unit/price.test.js
├── tests/component/cart.behaviour.test.jsx
├── tests/api/orders.test.js
├── e2e/checkout.spec.js
├── vitest.config.js
└── playwright.config.js
API স্তরের একটি test পুরোটা দেখুন। এখানে আসল Express route, আসল SQL আর আসল transaction চলে; শুধু network-এর বদলে supertest app-কে সরাসরি ডাকে।
beforeEach(() => {
db = openDb(); // a fresh in-memory database for every test
app = makeApp(db);
});
it('refuses more than the stock and changes nothing', async () => {
const res = await request(app).post('/api/orders').set(auth)
.send({ items: [{ productId: 1, qty: 1 }, { productId: 2, qty: 2 }] });
expect(res.status).toBe(409);
expect(stockOf(1)).toBe(10); // the first line was rolled back
expect(stockOf(2)).toBe(1);
});আর একমাত্র E2E test, যা Playwright নিজেই server চালু করে, Edge খোলে, ক্লিক করে, শেষে server বন্ধ করে:
test('a shopper adds rice and places an order', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Chinigura rice 5kg কার্টে যোগ করুন' }).click();
await page.getByRole('button', { name: 'অর্ডার করুন' }).click();
await expect(page.getByRole('status')).toHaveText('অর্ডার নিশ্চিত, মোট ৳510');
});প্রথমে Vitest-এর তিন স্তর একসাথে, সত্যিকারের run:
~/bazarghar$ npx vitest run --reporter=verbose
RUN v5.1.0 ~/bazarghar
✓ tests/unit/price.test.js > calcTotal > returns all zeros for an empty cart 1ms
✓ tests/unit/price.test.js > calcTotal > charges 60 taka delivery below 1000 taka 0ms
✓ tests/unit/price.test.js > calcTotal > gives free delivery above 1000 taka 0ms
✓ tests/unit/price.test.js > calcTotal > EID10 can push a cart back under the free-delivery line 0ms
✓ tests/unit/price.test.js > formatTaka > formats paisa as taka with Indian grouping 11ms
✓ tests/api/orders.test.js > POST /api/orders > creates the order and takes the stock 26ms
✓ tests/api/orders.test.js > POST /api/orders > refuses more than the stock and changes nothing 4ms
✓ tests/api/orders.test.js > POST /api/orders > ignores a price sent by the client 3ms
✓ tests/api/orders.test.js > POST /api/orders > needs a login 3ms
✓ tests/component/cart.behaviour.test.jsx > CartSummary (what the shopper sees and does) > shows the total and free delivery 25ms
✓ tests/component/cart.behaviour.test.jsx > CartSummary (what the shopper sees and does) > a coupon changes the total the order is placed with 216ms
✓ tests/component/cart.behaviour.test.jsx > CartSummary (what the shopper sees and does) > an empty cart cannot be ordered 4ms
Test Files 3 passed (3)
Tests 12 passed (12)
Duration 1.06s (environment 42%, tests 22%, setup 20%, import 13%, transform 2%, worker 1%)
তারপর Playwright, সত্যিকারের run:
~/bazarghar$ npx playwright test --reporter=list
Running 1 test using 1 worker
ok 1 e2e\checkout.spec.js:3:1 › a shopper adds rice and places an order (373ms)
1 passed (2.9s)
প্রতিটি স্তর আলাদা করে চালিয়ে পাওয়া সময় নিচে। এগুলো আমার ল্যাবের একটি local run; আপনার যন্ত্রে সংখ্যা আলাদা হবে, তবে অনুপাত মোটামুটি এমনই থাকার কথা।
| স্তর | Test | প্রতি test | পুরো run | সময় কোথায় গেল |
|---|---|---|---|---|
| Unit | ৫ | ০–১০ ms | 205ms | setup 76% |
| Component | ৩ | ৫–২৩০ ms | 1.10s | environment 56% (jsdom তৈরি) |
| API + DB | ৪ | ২–২৬ ms | 327ms | import 42% |
| E2E | ১ | ৩৭৩ ms | 2.9s | server চালু আর browser খোলা |
তিনটি জিনিস লক্ষ করুন। এক, unit test এত দ্রুত যে সময়ের বেশিরভাগ যায় runner চালু করতে। দুই, component test-এর ২৩০ ms-এর প্রায় সবটা user.type-এর পাঁচটি অক্ষর এক এক করে টাইপ করায়; Testing Library সত্যিকারের keystroke-এর মতো event পাঠায়। তিন, এখানে API test প্রতি test-এ component test-এর চেয়েও দ্রুত, কারণ database memory-তে। “Integration মানেই ধীর” কথাটি সবসময় সত্য নয়। আর একমাত্র E2E test-টি একাই বাকি বারোটির মোট সময়ের চেয়ে বেশি নিল।
Playwright-এর webServer নিজে server চালু করে আর শেষে বন্ধ করে। নিজে হাতে server চালালে কাজ শেষে শুধু নিজের চালানো process-টিই তার ID দিয়ে বন্ধ করুন। নাম ধরে সব node বন্ধ করলে একই যন্ত্রে চলা অন্যদের কাজও বন্ধ হয়ে যায়। Server বসান শুধু 127.0.0.1-এ।
৭ · Test-এর নাম, Arrange-Act-Assert আর ছোট দলের test budget
নাম একটি বাক্য
Test ফেল করলে প্রথমে চোখে পড়ে তার নাম। নামটি পড়েই বোঝা উচিত কোন আচরণ ভাঙল। উপরের run-এ calcTotal > EID10 can push a cart back under the free-delivery line পড়লেই নিয়মটি জানা যায়, কোড না খুলেই।
| দুর্বল নাম | ভালো নাম | কেন |
|---|---|---|
test1 | charges 60 taka delivery below 1000 taka | নিয়মটি নামেই লেখা |
should work correctly | refuses more than the stock and changes nothing | “ঠিক” কী, সেটি বলা |
calls updateStock | creates the order and takes the stock | ফল বলে, ভেতরের ডাক নয় |
তিন অংশের আকার
প্রায় প্রতিটি ভালো test তিন ভাগে পড়া যায়, যাকে বলে Arrange-Act-Assert (AAA). Arrange: শুরুর অবস্থা সাজানো। Act: একটিমাত্র কাজ করা। Assert: ফল মিলিয়ে দেখা। ধারণাটি সাধারণত Bill Wake-এর নামে বলা হয় (আনুমানিক ২০০১); BDD-তে একই আকারের নাম Given-When-Then. বিস্তারিত আছে unit testing পর্বের অংশ ২-এ.
it('gives free delivery above 1000 taka', () => {
// Arrange
const items = [rice, dal]; // 900 + 150 = 1050 taka
// Act
const t = calcTotal(items);
// Assert
expect(t.delivery).toBe(0);
expect(t.total).toBe(105000);
});একটি test-এ একাধিক Act থাকলে সাধারণত সেটি দুটি test. আর Assert-এ if বা loop দেখলে সন্দেহ করুন: test-এর ভেতরে যুক্তি থাকলে সেই যুক্তিরও test লাগবে।
দুই-তিনজনের দলের test budget
ছোট দলে সময়ই আসল সীমা। নিচের নিয়মগুলো একটি শুরুর বিন্দু, কোনো মান নয়; সংখ্যাগুলো ধরে নেওয়া।
- ঝুঁকির প্রথম তিন feature-এ বেশি সময়। টাকা, স্টক আর অনুমতির প্রতিটি নিয়মে unit test, আর প্রতিটি route-এ অন্তত একটি API test, যাতে “যা পাওয়ার কথা নয়” ঘরটিও আছে (পর্ব ৪৮-এর ছক)।
- প্রতিটি বাগের সাথে একটি test. বাগ ঠিক করার আগে এমন test লিখুন যা বাগটি দেখায় আর লাল হয়। এটিই সবচেয়ে কাজের regression test.
- E2E এক হাতের আঙুলে গোনা। কেনাকাটা, login, আর হয়তো seller-এর পণ্য যোগ। এর বেশি হলে প্রতিটির জন্য কারণ লিখুন।
- সময়ের সীমা। ধরা যাক, প্রতিটি push-এ পুরো suite পাঁচ মিনিটের মধ্যে। সীমা পার হলে নতুন test নয়, আগে ধীর test সারান।
- অস্থির test-এর নিয়ম। কোনো কোড না বদলেও কখনো পাস, কখনো ফেল করা test-কে বলে flaky test. এমন test হয় সেদিনই সারান, নয় আলাদা করে রাখুন; কারণ আর সমাধান আসবে পর্ব ৭৭-এ।
৮ · Coverage — Vitest-এর v8 provider আর সংখ্যার সীমা
Code coverage মাপে test চলার সময় কোডের কোন অংশ অন্তত একবার চলেছে। চার রকম সংখ্যা আসে: statement, branch, function আর line. Vitest দুটি provider দেয়। v8 provider JavaScript engine V8-এর নিজস্ব গোনার ব্যবস্থা ব্যবহার করে, কোডে বাড়তি কিছু ঢোকায় না। istanbul provider কোডে গোনার লাইন বসিয়ে (instrument করে) মাপে। তত্ত্ব, যেমন statement বনাম branch আর MC/DC, আছে coverage ও complexity পর্বে.
test: {
coverage: {
provider: 'v8',
include: ['src/**'],
exclude: ['src/server.js'],
reporter: [['text', { skipFull: false }]],
},
},আমার প্রথম run-এ ১০০% ফাইলগুলো text report-এ দেখায়নি; skipFull: false দেওয়ার পরে দেখাল। সত্যিকারের run:
~/bazarghar$ npx vitest run --coverage
% Coverage report from v8
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 92.85 | 92.85 | 90.9 | 92.15 |
CartSummary.jsx | 100 | 100 | 100 | 100 |
app.js | 89.18 | 88.23 | 80 | 88.57 | 27,35,55-56
price.js | 100 | 100 | 100 | 100 |
-----------------|---------|----------|---------|---------|-------------------
সংখ্যার চেয়ে কাজের হলো শেষ কলাম। app.js-এর ২৭ নম্বর লাইন GET /api/products; Vitest-এর কোনো test এটি ডাকে না, শুধু E2E ডাকে, আর E2E এই হিসাবে নেই। ৩৫ নম্বর লাইন 422-এর পথ: ভুল ইনপুট পাঠানো test নেই। ৫৫-৫৬ হলো catch-এর ভেতরের ROLLBACK, যা অপ্রত্যাশিত error-এ চলে। Coverage এভাবে বলে কোথায় test নেই. এখন প্রতিটি লাইনের জন্য সিদ্ধান্ত নিন: 422-এর পথে একটি test সস্তা আর কাজের; catch-এর পথ test করতে database-এ ভুল ঘটাতে হবে, তাই হয়তো পর্ব ৭৫-এর মতো আলাদা ব্যবস্থায়।
১০০% covered, তবু ভুল
উল্টো দিকটি আরও জরুরি: coverage বলে না, যা চলেছে তা ঠিক কি না। পরীক্ষাটি করলাম। price.js-এর নিয়ম “১০০০ টাকা বা তার বেশি হলে ফ্রি ডেলিভারি”; আমি ইচ্ছা করে একটি অক্ষর মুছলাম:
- const delivery = afterDiscount >= FREE_DELIVERY_MIN ? 0 : DELIVERY_FEE;
+ const delivery = afterDiscount > FREE_DELIVERY_MIN ? 0 : DELIVERY_FEE;~/bazarghar$ npx vitest run tests/unit --coverage --coverage.include=src/price.js
Test Files 1 passed (1)
Tests 5 passed (5)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
price.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
সব সবুজ, সব ১০০. অথচ ঠিক ১০০০ টাকার কার্টে ক্রেতা এখন ৬০ টাকা ডেলিভারি দেবেন। দুটি branch-ই চলেছে, ১৫০ টাকার কার্টে একটি আর ১০৫০ টাকার কার্টে অন্যটি, কিন্তু সীমানার মানটি কোনো test দেয়নি। এই কিনারার মান বাছাইয়ের কৌশলকে বলে boundary value analysis, বিস্তারিত test design পর্বের অংশ ৩-এ. একটি test যোগ করলাম:
~/bazarghar$ npx vitest run -c vitest.boundary.config.js
FAIL tests/boundary/boundary.test.js > exactly 1000 taka gets free delivery
AssertionError: expected 6000 to be +0 // Object.is equality
- Expected
+ Received
- 0
+ 6000
Tests 1 failed | 5 passed (6)
তাহলে লক্ষ্য কত?
১০০% coverage লক্ষ্য বানালে দুটি বিপদ। এক, শেষ কয়েক শতাংশ সাধারণত সবচেয়ে দামি: error-এর পথ, ঘটানো কঠিন ঘটনা। দুই, সংখ্যা লক্ষ্য হলে মানুষ সংখ্যা বাড়ায়, মান নয়: assertion ছাড়া test-ও coverage বাড়ায়। অর্থনীতিবিদ Charles Goodhart-এর নামে পরিচিত এই কথাটির সারাংশ: কোনো মাপ লক্ষ্য হয়ে উঠলে সেটি আর ভালো মাপ থাকে না। এর নাম Goodhart's law.
ব্যবহারিক পথ: coverage-কে মেঝে হিসেবে ব্যবহার করুন, ছাদ হিসেবে নয়। ঝুঁকিপূর্ণ ফাইলে, যেমন price.js-এ, Vitest-এর coverage.thresholds দিয়ে উঁচু সীমা রাখুন, যাতে কেউ test মুছলে CI জানায়। নতুন কোডের coverage কমছে কি না দেখুন। আর সবচেয়ে বেশি দেখুন “Uncovered Line” কলাম, শতাংশ নয়।
৯ · Mutation testing — test-এর test
আগের অংশে নিজে হাতে একটি ভুল ঢুকিয়ে দেখলাম test সেটি ধরে কি না। এই কাজটিই স্বয়ংক্রিয়ভাবে করার নাম mutation testing. একটি tool কোডে ছোট ছোট বদল আনে, যেমন >= থেকে >, - থেকে +, true থেকে false. প্রতিটি বদলানো রূপকে বলে mutant. তারপর প্রতিটি mutant-এর ওপর পুরো suite চালায়। কোনো test লাল হলে mutant “killed”; সব সবুজ থাকলে “survived”, অর্থাৎ এমন একটি ভুল আছে যা আপনার test দেখতে পায় না। মোট mutant-এর কত ভাগ মারা গেল, সেটি mutation score.
পুরো ধারণাটি ৩০ লাইনে, শেখার মডেল হিসেবে। একটি দুর্বল suite আর একটি শক্ত suite, একই চারটি mutant-এর বিরুদ্ধে:
// A tiny mutation tester: change one thing, run the tests, see who notices.
const source = `(subtotal, discount) => {
const after = subtotal - discount;
const delivery = after >= 100000 ? 0 : 6000;
return after + delivery;
}`;
const mutants = [
['>= to >', source.replace('>=', '>')],
['>= to <=', source.replace('>=', '<=')],
['- to +', source.replace('subtotal - discount', 'subtotal + discount')],
['6000 to 0', source.replace('6000', '0')],
];
const weak = [
['dal 150 taka', (f) => f(15000, 0) === 21000],
['cart 1050 taka', (f) => f(105000, 0) === 105000],
];
const strong = [...weak,
['exactly 1000 taka', (f) => f(100000, 0) === 100000],
['EID10 on 1050', (f) => f(105000, 10500) === 100500],
];
const original = new Function('return ' + source)();
for (const [suiteName, suite] of [['weak', weak], ['strong', strong]]) {
const greenOnOriginal = suite.every(([, t]) => t(original));
console.log(`--- ${suiteName} suite: ${suite.length} tests, original passes: ${greenOnOriginal}`);
let killed = 0;
for (const [name, code] of mutants) {
const f = new Function('return ' + code)();
const catchers = suite.filter(([, t]) => !t(f)).map(([n]) => n);
if (catchers.length) killed++;
console.log(name.padEnd(9), catchers.length ? 'KILLED by ' + catchers.join(', ') : 'SURVIVED');
}
console.log(`score ${killed}/${mutants.length}`);
}দুর্বল suite-এ >= to > আর - to + দুটিই SURVIVED, ফল score 2/4. দ্বিতীয়টি বেঁচে গেল কারণ দুর্বল suite-এ কোনো test-এ ছাড় নেই, ছাড় ০ হলে বিয়োগ আর যোগ একই। শক্ত suite-এ >= to > KILLED by exactly 1000 taka আর - to + KILLED by EID10 on 1050, ফল score 4/4. দুই suite-এরই আসল কোডে original passes: true; এই যাচাইটি জরুরি, কারণ আসল কোডেই লাল suite দিয়ে mutant মাপা অর্থহীন।
সীমানার mutant-টি কীভাবে বেঁচে যায়, ধাপে ধাপে দেখুন। মানগুলো আসল Stryker run-এর test থেকে নেওয়া।
1mutate: after >= 100000 → after > 1000002test: dal, 150 taka3test: rice + dal, 1050 taka4test: rice + dal with EID105report: Survived6new test: exactly 1000 taka
| after | |
| আসল delivery | |
| mutant delivery | |
| ফল |
- Mutant তৈরি। Stryker
price.js-এ মোট ২৬টি mutant বানাল। এটি তার একটি: ১২ নম্বর লাইনে>=হলো>. ঠিক সেই ভুল, যা অংশ ৮-এ হাতে ঢুকিয়েছিলাম। - ১৫০ টাকার কার্ট।
after15000, যা ১০০০০০-এর অনেক নিচে।>=হোক বা>, দুটিতেই উত্তর না, ডেলিভারি 6000. Test পাস করল। - ১০৫০ টাকার কার্ট।
after105000, সীমার বেশ ওপরে। দুই কোডেই উত্তর হ্যাঁ, ডেলিভারি 0. আবার পাস। - কুপনসহ। ছাড়ের পরে 94500, আবার সীমার নিচে, দুই কোডেই 6000. তিনটি test-ই সীমা থেকে দূরে, তাই পার্থক্য দেখতে পায় না।
- Survived. কোনো test লাল হয়নি। Stryker বলছে: আপনার suite এই ভুল আর আসল কোডের মধ্যে পার্থক্য করতে পারে না। অথচ coverage এই লাইনে ১০০% ছিল।
- সীমানার test. ঠিক 100000-এ আসল কোড বলে 0, mutant বলে 6000. মান আলাদা, test লাল, mutant killed. একটি test যোগ করেই score বাড়ল।
Stryker দিয়ে সত্যিকারের run
JavaScript-এর জনপ্রিয় mutation tool Stryker. লেখার সময়ে @stryker-mutator/core আর @stryker-mutator/vitest-runner দুটিই 10.1.1, আর runner-এর peer dependency vitest >=2.0.0, তাই Vitest 5.1.0-এর সাথে চলে। পুরো project নয়, শুধু একটি ছোট ফাইলে চালালাম:
{
"testRunner": "vitest",
"vitest": { "configFile": "vitest.unit.config.js", "related": false },
"mutate": ["src/price.js"],
"reporters": ["clear-text", "progress"],
"coverageAnalysis": "perTest"
}~/bazarghar$ npx stryker run
INFO Instrumenter Instrumented 1 source file(s) with 26 mutant(s)
INFO DryRunExecutor Initial test run succeeded. Ran 5 tests in 0 seconds ...
…
[Survived] EqualityOperator
src/price.js:12:20
- const delivery = afterDiscount >= FREE_DELIVERY_MIN ? 0 : DELIVERY_FEE;
+ const delivery = afterDiscount > FREE_DELIVERY_MIN ? 0 : DELIVERY_FEE;
[Survived] ObjectLiteral
src/price.js:17:54
- return '৳' + (paisa / 100).toLocaleString('en-IN', { maximumFractionDigits: 2 });
+ return '৳' + (paisa / 100).toLocaleString('en-IN', {});
File | total | covered | # killed | # timeout | # survived | # no cov | # errors |
All files | 92.31 | 92.31 | 24 | 0 | 2 | 0 | 0 |
INFO MutationTestExecutor Done in 2 seconds.
২৬টির মধ্যে ২৪টি মারা গেল: ২৪ ÷ ২৬ = ৯২.৩১%. সীমানার test যোগ করে আবার চালালে সত্যিকারের ফল 25 killed, 1 survived, score 96.15. বাকি একটি mutant কেন মরে না? toLocaleString-এ সংখ্যা দেখানোর ডিফল্ট সীমা তিন দশমিক ঘর। পয়সার পূর্ণসংখ্যাকে ১০০ দিয়ে ভাগ করলে দশমিকের পরে কখনো দুই ঘরের বেশি হয় না, তাই { maximumFractionDigits: 2 } থাকা আর না থাকা একই ফল দেয়। এমন mutant-কে বলে equivalent mutant: কোড বদলেছে, আচরণ বদলায়নি, তাই কোনো test তাকে মারতে পারবে না। Mutation testing-এর ফল তাই মানুষকে পড়ে বিচার করতে হয়; ১০০% score তাড়া করা এখানেও ভুল লক্ষ্য।
দাম মনে রাখুন: প্রতিটি mutant-এর জন্য test আবার চলে। এই ছোট ফাইলে ২ সেকেন্ড লাগল, বড় project-এ ঘণ্টা লাগতে পারে। তাই সাধারণ পথ হলো টাকা আর অনুমতির মতো গুরুত্বপূর্ণ কয়েকটি module-এ চালানো, অথবা শুধু বদলানো ফাইলে। Stryker-এর --incremental বিকল্প আগের ফল মনে রেখে শুধু বদলানো অংশ আবার চালায়; ব্যবহারের আগে আপনার version-এর docs দেখে নিন।
আরও গভীরে: Mutation testing-এর ধারণা ১৯৭০-এর দশকের শুরুর। Richard Lipton-এর ১৯৭১ সালের একটি লেখাকে প্রায়ই সূচনা ধরা হয়, আর DeMillo, Lipton ও Sayward-এর ১৯৭৮ সালের “Hints on Test Data Selection” লেখাটি এর দুটি ভিত্তি-অনুমান জনপ্রিয় করে: competent programmer hypothesis (আসল ভুল সাধারণত ঠিক কোড থেকে সামান্য দূরে) আর coupling effect (ছোট ভুল ধরা test বড় ভুলও ধরে)। Equivalent mutant নির্ণয় করা সাধারণভাবে undecidable, অর্থাৎ কোনো algorithm সব ক্ষেত্রে তা বলতে পারে না; তাই tool-গুলো অনুমান আর মানুষের বিচারের ওপর নির্ভর করে।
পরীক্ষা ও ইন্টারভিউতে: (১) Test pyramid আর testing trophy-র মূল পার্থক্য? Pyramid-এ সবচেয়ে বেশি unit, trophy-তে সবচেয়ে বেশি integration, ভিত্তিতে static. (২) ১০০% coverage মানে কি বাগ নেই? না, শুধু কোড চলেছে; assertion ঠিক কি না বলে না। (৩) Mutation score কীভাবে বের করেন? Killed ÷ মোট mutant; ২৬-এ ২৪ হলে ৯২.৩%. (৪) Brittle test কী? Implementation-এ বাঁধা test, যা আচরণ না বদলালেও ভাঙে। (৫) E2E কম রাখবেন কেন? ধীর, দামি, অস্থির, আর ফেল হলে কারণ খোঁজা কঠিন।
১০ · AI-এর যুগে test — পাস করে, অথচ কিছুই যাচাই করে না
AI-কে “calcTotal-এর test লিখে দাও” বললে সে মুহূর্তে test দেয়, আর সেগুলো সবুজও হয়। প্রশ্ন হলো, সেগুলো কিছু ধরে কি না। ল্যাবে এমন ধাঁচের তিনটি test লিখলাম, যেমনটা প্রায়ই দেখা যায়:
it('should calculate total correctly', () => {
const result = calcTotal(items, 'EID10');
expect(result).toBeDefined();
expect(result.total).toBeGreaterThan(0);
});
it('should handle a big cart', () => {
const r = calcTotal([{ pricePaisa: 300000, qty: 1 }]);
expect(r.total).toBe(r.subtotal - r.discount + r.delivery); // restates the code
});
it('should handle empty cart', () => {
expect(() => calcTotal([])).not.toThrow();
});সত্যিকারের run-এর ফল: তিনটিই পাস। price.js-এ coverage: statement 92.85, branch 100. দেখতে চমৎকার। এবার একই test দিয়ে Stryker:
~/bazarghar$ npx stryker run # same price.js, only the three tests above
[Survived] MethodExpression
- if (coupon === 'EID10') discount = Math.min(Math.round(subtotal * 0.1), EID_CAP);
+ if (coupon === 'EID10') discount = Math.max(Math.round(subtotal * 0.1), EID_CAP);
[Survived] ArithmeticOperator
- const afterDiscount = subtotal - discount;
+ const afterDiscount = subtotal + discount;
…
File | total | covered | # killed | # timeout | # survived | # no cov | # errors |
All files | 23.08 | 28.57 | 6 | 0 | 15 | 5 | 0 |
Branch coverage ১০০%, mutation score ২৩.০৮%. ছাড় যোগ হলেও, সীমা উল্টে Math.max হলেও, ডেলিভারির শর্ত উল্টে গেলেও কোনো test লাল হয় না। পাঁচটি mutant formatTaka-তে, যেখানে কোনো test পৌঁছায়ইনি (no cov); তাই মোট ২৬-এর মধ্যে ৬, কিন্তু যেখানে test পৌঁছেছে সেই ২১-এর মধ্যে ৬, অর্থাৎ ২৮.৫৭%. ফাঁকা test-এর সাধারণ চেহারাগুলো:
| লক্ষণ | কেন কিছু ধরে না | কী চাইবেন |
|---|---|---|
toBeDefined(), toBeGreaterThan(0) | প্রায় যেকোনো ভুল উত্তরও পাস | নির্দিষ্ট সংখ্যা, হাতে হিসাব করা |
| প্রত্যাশা কোডের সূত্র দিয়েই হিসাব | কোড ভুল হলে প্রত্যাশাও একই রকম ভুল | বাইরে থেকে জানা মান |
| যে function test হচ্ছে, তাকেই mock | নকলকে test করা হচ্ছে, আসলকে নয় | শুধু বাইরের সীমানায় নকল |
শুধু not.toThrow() | ভুল ফল দিলেও পাস | ফলের মান যাচাই |
| ফেল হলে test-এর প্রত্যাশা বদলে বাগের সাথে মেলানো | বাগটি test-এ পাকা হয়ে যায় | কোনটি ঠিক, নিয়ম দেখে ঠিক করা |
| Snapshot চোখ বন্ধ করে update | যা আছে তাই ঠিক ধরে নেয় | Diff পড়ে তবেই গ্রহণ |
AI-কে test লেখাতে চাইলে কোড নয়, নিয়ম আর কিনারা আগে দিন। তারপর ফল যাচাইয়ের দায় নিজের কাছে রাখুন।
AI-কে test লিখতে বলার একটি নমুনা prompt:
AI-এর লেখা test হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Pyramid আর trophy নিয়ে বিতর্ক এত পুরোনো হয়েও মেটে না কেন?
কারণ দুই পক্ষ আসলে ভিন্ন সময়ের, ভিন্ন ধরনের কোড দেখে কথা বলছেন। Mike Cohn-এর pyramid জনপ্রিয় হয় ২০০০-এর দশকের শেষে, যখন UI test মানে ছিল ধীর, ভঙ্গুর browser automation; একটি test চালাতে মিনিট লাগত, আর selector বদলালেই ভাঙত। সেই অবস্থায় “নিচে অনেক দ্রুত test, ওপরে অল্প” খুবই যুক্তিসংগত পরামর্শ। Martin Fowler ২০১২ সালে তাঁর ওয়েবসাইটে ছবিটি আরও ছড়িয়ে দেন।
২০১০-এর দশকে JavaScript frontend বদলে গেল। React-এর মতো component-ভিত্তিক UI এল, আর jsdom-এর মতো নকল browser-এ মিলিসেকেন্ডে component চালানো সম্ভব হলো। তখন দেখা গেল, প্রতিটি ছোট function আলাদা করে test করলে test-গুলো ভেতরের গঠনে বাঁধা পড়ে। ২০১৬ সালে Guillermo Rauch-এর একটি টুইট, “Write tests. Not too many. Mostly integration.”, এই অনুভূতির স্লোগান হয়ে যায়। Kent C. Dodds আনুমানিক ২০১৮ সালে trophy আঁকেন, যেখানে মাঝের অংশ সবচেয়ে মোটা। একই সময়ে Spotify-এর engineering blog microservice-এর জন্য honeycomb বর্ণনা করে।
বিতর্ক জিইয়ে রাখে নামের অস্পষ্টতাও। Trophy-র “integration” মানে প্রায়ই কয়েকটি component একসাথে, jsdom-এ, network ছাড়া; গতিতে সেটি pyramid-এর unit test-এর কাছাকাছি। আবার কেউ “integration” বলতে বোঝান আসল database আর অন্য service-সহ test, যা অনেক ধীর। একই শব্দে দুই জিনিস, তাই একজনের “বেশি integration” আরেকজনের কাছে “ধীর suite”.
ফুল-স্ট্যাক অ্যাপে বিতর্কটি অনেকটাই মিটে যায়, যদি অংশ ধরে ভাবেন। দাম আর ছাড়ের হিসাব pyramid চায়, React-এর পাতা trophy চায়, database-এ কথা বলা API honeycomb চায়। এই পর্বের ল্যাবে API test component test-এর চেয়েও দ্রুত চলেছে; তাই আকার না মুখস্থ করে প্রতিটি test-এর আসল গতি, স্থিরতা আর ধরতে পারা ভুলের তালিকা দেখুন।
মূল উপলব্ধি: প্রতিটি আকার একটি নির্দিষ্ট যুগের নির্দিষ্ট কোডের পরামর্শ। আকার নয়, ঝুঁকি কোথায় আর কোন test সবচেয়ে সস্তায় সেটি ধরে, সেটিই সিদ্ধান্তের ভিত্তি।
প্র ০২কিছু প্রকল্প সত্যিই ১০০% branch coverage দাবি করে। তাহলে এই পর্বে ১০০%-কে লক্ষ্য না বানাতে বলা হলো কেন?
কারণ ১০০% coverage দুই ধরনের প্রতিষ্ঠানে দুই রকম অর্থ বহন করে। একদিকে আছে সেই সফটওয়্যার, যার ভুলের দাম মানুষের জীবন বা কোটি মানুষের ডেটা। বিমানের সফটওয়্যারের মান DO-178C সবচেয়ে গুরুত্বপূর্ণ স্তরের কোডে MC/DC নামের কঠোর coverage চায়, যেখানে প্রতিটি শর্ত আলাদাভাবে ফলের ওপর প্রভাব ফেলে কি না দেখাতে হয়। SQLite-এর দল তাদের ওয়েবসাইটে জানায়, তাদের test harness ১০০% branch coverage আর MC/DC অর্জন করে। এসব প্রকল্পে coverage একটি বড় পদ্ধতির অংশ: আলাদা স্বাধীন যাচাই, review, আর বহু বছরের শৃঙ্খলা।
অন্যদিকে সাধারণ ব্যবসার ওয়েব অ্যাপ। সেখানে ১০০% বাধ্যতামূলক করলে প্রায়ই উল্টো ফল হয়। শেষ কয়েক শতাংশ পেতে লাগে কঠিন error-এর পথ সাজানো, যার জন্য জটিল mock বানাতে হয়, আর সেই mock ভেতরের গঠনে বাঁধা পড়ে। আরও খারাপ, সংখ্যা লক্ষ্য হয়ে গেলে মানুষ সংখ্যা মেলায়: assertion ছাড়া একটি test-ও লাইনগুলো “চালিয়ে” দেয়, coverage বাড়ায়। Charles Goodhart ১৯৭৫ সালে অর্থনীতির প্রসঙ্গে যে পর্যবেক্ষণ দেন, পরে Marilyn Strathern-এর ভাষায় যা জনপ্রিয় হয়, তার সারকথা এটিই: মাপ লক্ষ্য হলে মাপ নষ্ট হয়।
এই পর্বের ল্যাবেই দুটি প্রমাণ আছে। > বসানো ভুল কোডে coverage ছিল পুরো ১০০, সব test সবুজ। আর AI-ধাঁচের ফাঁপা test-এ branch coverage ১০০%, অথচ mutation score মাত্র ২৩%. দুই ক্ষেত্রেই সংখ্যাটি মিথ্যা বলেনি, কোড সত্যিই চলেছিল। সমস্যা হলো আমরা সংখ্যাটিকে এমন প্রশ্নের উত্তর ভেবেছি, যার উত্তর সে দেয় না।
তাই কাজের পথ হলো ঝুঁকি অনুযায়ী মেঝে। টাকার হিসাবের ফাইলে উঁচু সীমা আর mutation testing, সাধারণ UI-তে মাঝারি সীমা, আর সবসময় “কোন লাইন চলেনি” তালিকাটি পড়ে সিদ্ধান্ত নেওয়া।
মূল উপলব্ধি: Coverage বলে কোথায় test নেই, বলে না যেখানে test আছে সেটি কাজের কি না। উঁচু coverage তখনই অর্থবহ, যখন তার পেছনে কঠোর assertion আর আলাদা যাচাই থাকে।
প্র ০৩২০১২ সালের Knight Capital-এর ঘটনা থেকে test আর deploy-এর আস্থা নিয়ে কী শেখা যায়?
২০১২ সালের ১ আগস্ট যুক্তরাষ্ট্রের শেয়ারবাজারের বড় একটি trading প্রতিষ্ঠান Knight Capital প্রায় ৪৫ মিনিটে বিপুল ক্ষতির মুখে পড়ে। মার্কিন নিয়ন্ত্রক SEC-এর ২০১৩ সালের আদেশে বর্ণনা অনুযায়ী, ক্ষতি ছিল ৪৬ কোটি ডলারের বেশি, আর প্রতিষ্ঠানটি কার্যত টিকে থাকতে পারেনি। ঘটনাটি software deploy-এর ইতিহাসে বহুল আলোচিত।
SEC-এর বর্ণনার মূল কথা এরকম। একটি নতুন feature-এর জন্য কোড আটটি server-এ বসানোর কথা ছিল, কিন্তু একটিতে বসেনি। নতুন কোড একটি পুরোনো flag-কে নতুন অর্থে ব্যবহার করছিল। যে server-এ পুরোনো কোড রয়ে গিয়েছিল, সেখানে সেই flag বহু বছর আগে বন্ধ হয়ে যাওয়া একটি পুরোনো feature চালু করে দিল, যা বাজারে অসংখ্য অনাকাঙ্ক্ষিত অর্ডার পাঠাতে থাকে। পুরোনো কোডটি মুছে ফেলা হয়নি, শুধু অব্যবহৃত পড়ে ছিল।
শিক্ষাগুলো বহুমুখী, আর সব test-এর নয়। প্রথমত, unit test এখানে সাহায্য করত না: নতুন কোড হয়তো নিজে ঠিকই ছিল। ভুল ছিল জোড়ায়: একটি পুরোনো কোড আর একটি নতুন অর্থের flag একসাথে। এ ধরনের ভুল ধরা পড়ে শুধু সত্যিকারের পরিবেশে পুরো পথ চালিয়ে দেখলে, অর্থাৎ deploy-এর পরে একটি smoke check বা ছোট E2E দিয়ে। দ্বিতীয়ত, মৃত কোড ঝুঁকি; যে কোড আর চলে না, তা মুছে ফেলা নিরাপদ, আর ভালো test থাকলে সেই মুছে ফেলার সাহস আসে। তৃতীয়ত, deploy নিজেই স্বয়ংক্রিয় আর যাচাইযোগ্য হওয়া দরকার: সব server-এ একই version আছে কি না, যন্ত্র নিজে দেখবে।
বাজারঘরের মাপে ভাবুন। ঈদের অফারের flag চালু করলেন, আর একটি পুরোনো instance-এ পুরোনো কোড রয়ে গেল। এক মুহূর্তে কিছু ক্রেতা ছাড় পেলেন, কিছু পেলেন না, কিছু অর্ডারে ছাড় দুবার। এর বিরুদ্ধে প্রতিরক্ষা কয়েক স্তরের: হিসাবের unit test, API test, deploy-এর পরে একটি স্বয়ংক্রিয় checkout E2E, আর পর্ব ৮৩-এর monitoring.
মূল উপলব্ধি: Deploy-এর আস্থা আসে test, স্বয়ংক্রিয় deploy আর deploy-পরবর্তী যাচাই মিলিয়ে। কোনো একটি স্তর একা যথেষ্ট নয়, আর জোড়ার ভুল ধরার জন্য অল্প কিছু ওপরের স্তরের test সবসময় লাগে।
প্র ০৪Mutation testing প্রায় ৫০ বছরের পুরোনো ধারণা। তবু সব দল এটি নিয়মিত চালায় না কেন?
মূল বাধা তিনটি: সময়, equivalent mutant, আর ফল ব্যবহারের পথ। প্রথমত সময়। প্রতিটি mutant-এর জন্য test আবার চালাতে হয়। আমাদের ছোট price.js-এ ২৬টি mutant আর পাঁচটি দ্রুত test, তাই ২ সেকেন্ড। কিন্তু হাজার ফাইলের project-এ লক্ষাধিক mutant হতে পারে, আর suite যদি কয়েক মিনিটের হয়, পুরো হিসাব দিনের পর দিন লাগবে। আধুনিক tool-গুলো এই খরচ কমায়: প্রতিটি mutant-এর জন্য শুধু সেই test চালায় যা ওই লাইন ছোঁয় (Stryker-এর perTest coverage analysis), আগের ফল মনে রাখে, আর একসাথে অনেক process চালায়।
দ্বিতীয়ত equivalent mutant. আমাদের maximumFractionDigits-এর mutant কোনো test মারতে পারবে না, কারণ আচরণ বদলায়নি। কোনটি equivalent আর কোনটি আসলে দুর্বল test-এর লক্ষণ, তা বুঝতে একজন মানুষকে কোড পড়তে হয়। Survived-এর তালিকা যদি এমন “শব্দ”-এ ভরে যায়, ডেভেলপাররা তালিকা পড়াই বন্ধ করে দেন।
তৃতীয়ত ফল কোথায় দেখাবেন। পুরো project-এর একটি score মাসে একবার দেখলে কেউ কিছু করে না। Google-এর গবেষকেরা ২০১৮ সালের দিকে প্রকাশিত একটি গবেষণাপত্রে জানান, তাঁরা পুরো project-এর score-এর বদলে code review-এর সময়, শুধু বদলানো লাইনে, বাছাই করা কিছু mutant দেখান, আর অপ্রয়োজনীয় ধরনের mutant বাদ দেন। এতে ফলটি সঠিক মুহূর্তে সঠিক মানুষের কাছে পৌঁছায়।
ছোট দলের জন্য ব্যবহারিক পথ তাই নির্বাচিত ব্যবহার। বাজারঘরে দাম, ছাড়, স্টক আর অনুমতির নিয়ম যে কয়েকটি ফাইলে, সেখানে মাঝে মাঝে Stryker চালানো; survived mutant দেখে সীমানার test যোগ করা; আর AI-লেখা test গ্রহণের আগে একবার চালিয়ে দেখা সেগুলো ফাঁপা কি না। পুরো project-এ প্রতিটি push-এ চালানো ছোট দলের জন্য সাধারণত দরকার নেই।
মূল উপলব্ধি: Mutation testing দামি কিন্তু তীক্ষ্ণ যন্ত্র। গুরুত্বপূর্ণ অল্প কোডে, বদলানো লাইনে আর সন্দেহজনক test-এ ব্যবহার করলে এটি এমন দুর্বলতা দেখায়, যা coverage কখনো দেখায় না।
প্র ০৫Implementation-এর খুঁটিনাটি test করা কি সবসময় ভুল? কখন সেটি ঠিক সিদ্ধান্ত?
না, সবসময় ভুল নয়। প্রশ্নটি আসলে “এটি কি কারও কাছে চুক্তি?” যে ভেতরের জিনিস বদলালে ব্যবহারকারী, অন্য দল বা খরচের হিসাব ক্ষতিগ্রস্ত হয়, সেটি test-এ বাঁধা যুক্তিসংগত। যা বদলালে কারও কিছু যায় আসে না, সেটি বাঁধলে শুধু ভঙ্গুরতা বাড়ে।
কয়েকটি বৈধ উদাহরণ। পর্ব ৪২-এর query budget test গোনে একটি পাতায় কতগুলো SQL চলল। ক্রেতা SQL দেখেন না, কিন্তু N+1 ফিরে এলে পাতা ধীর হয় আর database-এর খরচ বাড়ে; এটি performance-এর চুক্তি। Webhook-এর ক্ষেত্রে, payment gateway-কে ঠিক একবার ডাকা হলো কি না দেখা জরুরি, কারণ দুবার ডাকলে টাকা দুবার কাটতে পারে; এখানে mock দিয়ে “কতবার ডাকা হলো” যাচাই আসলে বাইরের আচরণই। পর্ব ৪৮-এর ছকে ভুল অনুরোধে database-এ কিছু বদলায়নি, এটিও যাচাই করা হয়েছে; ভেতরের অবস্থা দেখা হলেও সেটি নিরাপত্তার চুক্তি।
পুরোনো কোডের ক্ষেত্রে আরেকটি পরিস্থিতি। Michael Feathers তাঁর Working Effectively with Legacy Code (২০০৪) বইয়ে characterization test-এর কথা বলেন: কোড এখন যা করে, ঠিক বা ভুল যা-ই হোক, সেটি লিখে রাখা, যাতে refactor করার সময় অজান্তে কিছু না বদলায়। এগুলো ইচ্ছা করেই খুঁটিনাটিতে বাঁধা, আর refactor শেষে অনেকগুলো মুছে আচরণভিত্তিক test দিয়ে বদলানো হয়।
তাহলে কোথায় সীমা টানবেন? একটি সহজ পরীক্ষা: এমন একটি refactor কল্পনা করুন যা ব্যবহারকারীর কাছে কিছুই বদলায় না, যেমন class-এর নাম বদল, helper ভেঙে দুটি করা, useState থেকে useReducer. Test যদি তাতে ভাঙে, আর সেই ভাঙার পেছনে কোনো চুক্তি না থাকে, তবে test-টি ভঙ্গুর। এই পর্বের brittle test ঠিক এভাবে ভেঙেছিল, আবার আসল বাগে চুপ ছিল।
মূল উপলব্ধি: নিয়মটি “implementation কখনো নয়” নয়; নিয়মটি হলো “যা চুক্তি, শুধু সেটিই”. চুক্তি হতে পারে ব্যবহারকারীর চোখের আচরণ, performance-এর সীমা, নিরাপত্তা বা বাইরের সেবার সাথে কথা।
অনুশীলন
হাতে হিসাব, নকশার সমালোচনা, আউটপুট আগাম বলা আর নিজের project-এ কমান্ড।
-
হিসাব: ধরে নিন প্রতিটি E2E test স্বাধীনভাবে ১% সম্ভাবনায় কোনো বাগ ছাড়াই ফেল করে। (ক) ১০টি E2E থাকলে একটি run-এ অন্তত একটি মিথ্যা লাল হওয়ার সম্ভাবনা কত? (খ) ৫০টি থাকলে? (গ) ১০টি test-এর প্রতিটির অস্থিরতা ০.৫%-এ নামালে (ক)-এর উত্তর কত হবে?
সূত্র: অন্তত একটি ফেল = ১ − (সবগুলো পাস) = ১ − (১ − p)n. (ক) ১ − ০.৯৯১০ ≈ ১ − ০.৯০৪৪ = ৯.৫৬%, অর্থাৎ প্রায় প্রতি ১০টি run-এ একটিতে মিথ্যা লাল। (খ) ১ − ০.৯৯৫০ ≈ ১ − ০.৬০৫০ = ৩৯.৫%. প্রায় প্রতি পাঁচটির দুটি build কোনো কারণ ছাড়াই লাল, আর দল লাল রং বিশ্বাস করা বন্ধ করে দেবে। (গ) ১ − ০.৯৯৫১০ ≈ ৪.৮৯%. শিক্ষা: E2E-র সংখ্যা বাড়লে অস্থিরতা গুণিতকে বাড়ে, তাই E2E কম রাখা আর প্রতিটিকে স্থির করা দুটোই জরুরি।
-
নকশা ও সমালোচনা: একজন সহকর্মী বাজারঘরের অর্ডার API-র জন্য নিচের test পাঠালেন। এতে কী কী দুর্বলতা?
vi.mock('../src/db.js'); it('creates order', async () => { db.insertOrder.mockResolvedValue({ id: 1 }); const res = await request(app).post('/api/orders').send({ items: [{ productId: 1, qty: 1 }] }); expect(res.status).toBeDefined(); expect(db.insertOrder).toHaveBeenCalled(); });- Database পুরো নকল: স্টক কমল কি না, transaction rollback হয় কি না, SQL ঠিক কি না, কিছুই যাচাই হয় না। এই route-এর ঝুঁকি ঠিক database-এ, তাই আসল test database লাগবে।
- Assertion ফাঁপা:
toBeDefined()500-এও পাস। নির্দিষ্ট201আরtotalPaisa-এর হাতে হিসাব করা মান চাই। - ভেতরের ডাক যাচাই:
insertOrderডাকা হয়েছে কি না, এটি implementation. Function-এর নাম বদলালে ভাঙবে, অথচ ভুল দামে অর্ডার হলেও পাস করবে। - Login নেই, তবু পাস হতে পারে: Header-এ token নেই; route যদি login চাইত, test-এর 401-ও
toBeDefinedপার হতো। আলাদা “login ছাড়া 401” test লাগবে। - নেতিবাচক কেস নেই: স্টকের বেশি চাওয়া, client-এর পাঠানো দাম, ভুল qty, এসবের test নেই।
- নাম অস্পষ্ট:
creates order-এর বদলেcreates the order and takes the stock.
-
আউটপুট আগাম বলুন:
const free = (t) => t >= 100000;এর জন্য দুটি test আছে:free(150000) === trueআরfree(50000) === false. একটি mutation tool চারটি mutant বানাল:>=থেকে>,>=থেকে<=, পুরো শর্তtrue, আর পুরো শর্তfalse. কোনগুলো survived, আর score কত? কোন একটি test যোগ করলে সব মরবে?>: 150000 > 100000 সত্য, 50000 > 100000 মিথ্যা, দুটি test-ই পাস, তাই SURVIVED.<=: 150000-এ মিথ্যা, প্রথম test লাল, KILLED.true: 50000-এ সত্য, দ্বিতীয় test লাল, KILLED.false: 150000-এ মিথ্যা, প্রথম test লাল, KILLED. Score ৩/৪ = ৭৫%.free(100000) === trueযোগ করলে>mutant মিথ্যা দেবে, তাই সেটিও মরবে, score ৪/৪. পাতারtiny-mutation.jsblock-এ এই কেস বসিয়ে মিলিয়ে দেখতে পারেন। -
কমান্ড ও পরীক্ষা: নিজের কোনো Vite বা Node project-এ
@vitest/coverage-v8বসিয়েnpx vitest run --coverageচালান। “Uncovered Line” কলাম থেকে একটি লাইন বেছে ঠিক করুন সেখানে test লাগবে কি না। তারপর একটি গুরুত্বপূর্ণ ছোট ফাইলে Stryker চালান, আর একটি survived mutant-এর জন্য test লিখুন। কাজটি শুধু নিজের local project-এ করুন।আমাদের ল্যাবে coverage দিল
app.js | 89.18 | 88.23 | 80 | 88.57 | 27,35,55-56. সিদ্ধান্ত: ৩৫ নম্বর লাইনের 422-এর পথে একটি test লিখব, কারণ সস্তা আর ভুল ইনপুট ঠেকানো নিরাপত্তার কাজ। ২৭ নম্বর লাইন E2E ছোঁয়, তবু একটি ছোট API test দেওয়া ভালো, যাতে Vitest-এর হিসাবেও আসে। ৫৫-৫৬-এরcatchপথের জন্য database-এ ভুল ঘটাতে হবে, তাই পর্ব ৭৫-এর ব্যবস্থায় দেখব। Strykerprice.js-এ দিয়েছিল ২৬টি mutant, ২টি survived; সীমানার test যোগ করে96.15, আর বাকিটি equivalent mutant. আপনার project-এ ফল আলাদা হবে; মূল কথা হলো প্রতিটি survived mutant-এর সামনে প্রশ্ন করা: test দুর্বল, নাকি mutant আচরণই বদলায় না?
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নতুন feature-এর test পরিকল্পনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⚖️ ক্ষতি আর সম্ভাবনা দিয়ে ঝুঁকি মাপা
- 🧭 ভুলটি সবচেয়ে সস্তায় ধরে এমন স্তর বাছাই
- ✍️ আচরণ ধরে, নিয়মের নামে AAA আকারে test লেখা
- 🧪 ইচ্ছা করে ভুল ঢুকিয়ে বা mutation দিয়ে দেখা test লাল হয় কি না
- 🔁 প্রতিটি push-এ স্বয়ংক্রিয়ভাবে চালানো
একটি অনলাইন শপের নিয়ম: “EID10 কুপনে ১০% ছাড়, সর্বোচ্চ ২০০ টাকা।” নিয়মটি একটি খাঁটি function-এ লেখা, যা কোনো DOM বা database ছোঁয় না। এই সীমা যাচাইয়ের সবচেয়ে সস্তা আর উপযুক্ত স্তর কোনটি?
- Unit test
- E2E test
- Component test
- শুধু চোখে দেখা
একটি component test container.querySelector('.row.total span:last-child') দিয়ে মোট দাম পড়ে। Developer markup-কে dl, dt, dd-তে বদলালেন আর class-এর নাম বদলালেন; পাতায় দেখানো মোট আর বোতামের কাজ একই রইল। Test ফেল করল। এটিকে কী বলবেন?
- একটি আসল regression ধরা পড়েছে
- একটি flaky test
- Implementation-এ বাঁধা brittle test-এর মিথ্যা লাল
- Coverage কম হওয়ার লক্ষণ
একটি function-এ after >= 100000 ? 0 : 6000 ভুল করে after > 100000 লেখা হলো। Test দুটি: after = 15000 আর after = 105000. দুটিই পাস, branch coverage ১০০%. এ থেকে কী বোঝা যায়?
- ১০০% branch coverage থাকায় কোডে ভুল নেই
- Coverage বলে কোড চলেছে, ঠিক কি না বলে না; সীমানার মান 100000-এর test নেই
- আরও বেশি এলোমেলো মানের test দিলেই নিশ্চিতভাবে ধরা পড়ত
- v8 provider ভুল হিসাব দিয়েছে, istanbul দিলে ধরা পড়ত
একটি ফাইলে mutation tool ২৬টি mutant বানাল। ২৪টি কোনো না কোনো test-এ লাল হলো, ২টি সব test পার হয়ে গেল, timeout বা error নেই। Mutation score কত?
- প্রায় ৭.৭%
- ১০০%
- প্রায় ৮৪.৬%
- প্রায় ৯২.৩%
Kent C. Dodds-এর testing trophy-তে কোন স্তরটি সবচেয়ে বড় অংশ, আর তার ভিত্তিতে কী থাকে?
- সবচেয়ে বড় unit, ভিত্তিতে manual test
- সবচেয়ে বড় integration, ভিত্তিতে static check
- সবচেয়ে বড় E2E, ভিত্তিতে unit
- সবচেয়ে বড় contract test, ভিত্তিতে honeycomb
AI একটি দামের function-এর জন্য এই test দিল: ফল নিয়ে expect(result).toBeDefined() আর expect(result.total).toBeGreaterThan(0). Test পাস করে। মূল সমস্যা কী?
- Test-এর নাম ইংরেজিতে
- toBeDefined এই version-এ নেই
- প্রায় যেকোনো ভুল ফলও এই দুটি শর্ত পূরণ করে, তাই test বাগ ধরে না
- এটি coverage কমিয়ে দেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Test কেনে আস্থা: refactor-এর সাহস আর deploy-এর ভরসা। নাম নয়, দেখুন কোন অংশ আসল, কোনটি নকল।
- Pyramid, trophy, honeycomb ভিন্ন কোডের পরামর্শ; ফুল-স্ট্যাকে হিসাবে unit, UI-তে component, API-তে database-সহ test, আর অল্প E2E.
- ঝুঁকি আগে: টাকা, স্টক, login আর অনুমতি; footer-এর রং শেষে, বা test-ই নয়।
- আচরণ test করুন: brittle test নির্দোষ refactor-এ ভাঙে আর আসল বাগে চুপ থাকে।
- Coverage মেঝে, mutation আয়না: ১০০% coverage-এও সীমানার বাগ থাকে; survived mutant দুর্বল বা ফাঁপা test দেখায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭৩ · Unit test — Vitest, mock আর snapshotপরবর্তী পর্বVitest দিয়ে খাঁটি যুক্তির test, mock, নকল ঘড়ি আর snapshot.
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপরের পর্ব ৭৩: Unit test — Vitest, mock আর snapshot.
- পর্ব ৪৮ · Permissionআগের পর্বVitest আর supertest দিয়ে role বনাম route ছক।
- পর্ব ৬৫ · Payment integrationআগের পর্বনকল gateway-র বিরুদ্ধে Playwright checkout.
- Test pyramid আর integrationতত্ত্বআকারগুলোর ইতিহাস, contract test আর flaky test.
- Coverage আর complexityগভীরে যানStatement, branch, MC/DC আর mutation testing-এর তত্ত্ব।
- Test design techniquesপাশের ট্র্যাকEquivalence partitioning আর boundary value analysis.