JavaScript quiz বানান: logic আলাদা করে test করুন
Quiz থেকে array, function ও testing শেখা যায়। আসল শিক্ষা: logic পর্দা থেকে আলাদা রাখলে একই কোড terminal ও ওয়েব পাতায় চলে।

Quiz দেখতে নতুনদের খেলনা মনে হয়, কিন্তু এতে লুকানো আছে এমন শিক্ষা যা প্রতিটি আসল প্রজেক্টে লাগে: logic ও interface আলাদা রাখুন। নিয়মগুলো (shuffle, যাচাই, নম্বর) সাধারণ function-এ থাকলে একই কোড আজ terminal quiz চালাবে, কাল ওয়েব পাতা চালাবে, আর কিছু click না করেই test করা যাবে। কোড ও আউটপুট প্রকাশের আগে Node.js 22-এ চালানো হয়েছে।
আমরা কী বানাচ্ছি
- choice ও সঠিক উত্তরের index সহ প্রশ্নের তালিকা।
- এলোমেলো ক্রমের জন্য shuffle।
- স্কোর, শতকরা ও বার্তা ফেরত দেওয়া নম্বর গণনা।
- প্রশ্ন করে ভুল ইনপুট ফেরত দেওয়া ছোট terminal interface।
- logic-এর test।
ধাপ ১: logic (quiz.mjs)
export const questions = [
{ text: "Which keyword declares a variable that cannot be reassigned?", choices: ["var", "let", "const"], answer: 2 },
{ text: "What does Array.prototype.map return?", choices: ["A new array", "The same array", "undefined"], answer: 0 },
{ text: "What is the result of typeof null?", choices: ["'null'", "'object'", "'undefined'"], answer: 1 },
];
// Fisher-Yates shuffle. `random` is injectable so tests can control it.
export function shuffle(items, random = Math.random) {
const copy = [...items];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
export function isCorrect(question, choiceIndex) {
return choiceIndex === question.answer;
}
export function grade(questions, picks) {
const score = questions.filter((q, i) => isCorrect(q, picks[i])).length;
const percent = questions.length ? Math.round((score / questions.length) * 100) : 0;
const message = percent === 100 ? "Perfect!" : percent >= 60 ? "Good job." : "Keep practising.";
return { score, total: questions.length, percent, message };
}
এখানে কিছুই print করে না বা ইনপুট পড়ে না। প্রতিটি function data নেয় ও data ফেরত দেয়। এতেই test করা সহজ।
ধাপ ২: terminal interface (cli.mjs)
import { createInterface } from "node:readline";
import { questions, shuffle, grade } from "./quiz.mjs";
const lines = createInterface({ input: process.stdin })[Symbol.asyncIterator]();
async function ask(prompt) {
process.stdout.write(prompt);
const { value, done } = await lines.next();
if (done) throw new Error("Input ended before the quiz finished");
return value;
}
const asked = shuffle(questions);
const picks = [];
for (const [n, q] of asked.entries()) {
console.log(`\nQ${n + 1}. ${q.text}`);
q.choices.forEach((choice, i) => console.log(` ${i + 1}) ${choice}`));
let pick = NaN;
while (!(pick >= 0 && pick < q.choices.length)) {
pick = Number(await ask("Your answer: ")) - 1;
}
picks.push(pick);
}
const { score, total, percent, message } = grade(asked, picks);
console.log(`\nScore: ${score}/${total} (${percent}%). ${message}`);
process.exit(0);
চালিয়ে দেখুন
$ node cli.mjs
Q1. What does Array.prototype.map return?
1) A new array
2) The same array
3) undefined
Your answer: 1
Q2. What is the result of typeof null?
1) 'null'
2) 'object'
3) 'undefined'
Your answer: 9
Your answer: 2
Q3. Which keyword declares a variable that cannot be reassigned?
1) var
2) let
3) const
Your answer: 3
Score: 3/3 (100%). Perfect!
প্রতিবার প্রশ্নের ক্রম এলোমেলো। 9 দিলে তা প্রত্যাখ্যাত হয়ে প্রশ্নটি আবার আসে, কারণ pick সঠিক choice না হওয়া পর্যন্ত loop চলে।
এটি কীভাবে কাজ করে
Object হিসেবে data। প্রতিটি প্রশ্ন { text, choices, answer }। নতুন প্রশ্ন মানে একটি object যোগ, কোড বদলাতে হয় না।
Fisher-Yates shuffle। array-র শেষ থেকে হেঁটে প্রতিটি item-কে আগের কোনো এলোমেলো অবস্থানের সঙ্গে বদলান। এতে প্রতিটি ক্রমের সমান সম্ভাবনা। প্রচলিত শর্টকাট array.sort(() => Math.random() - 0.5) পক্ষপাতদুষ্ট, ন্যায্য shuffle-এ ব্যবহার করবেন না।
বদলানোর আগে কপি। [...items] array কপি করে, তাই shuffle মূল array বদলায় না। ইনপুট চুপচাপ বদলে দেওয়া function কঠিন bug বানায়।
ইনজেক্ট করা randomness। shuffle(items, random = Math.random) test-কে নিজের function দিতে দেয়, ফলে "এলোমেলো" ফল অনুমানযোগ্য ও test-যোগ্য হয়।
নম্বর গণনা। filter সঠিক উত্তর গোনে। খালি quiz-এ শূন্য দিয়ে ভাগ এড়াতে শতকরায় সুরক্ষা আছে।
ইনপুট যাচাই। Number("abc") হলো NaN, আর NaN >= 0 মিথ্যা, তাই while loop প্রশ্ন করতে থাকে। ব্যবহারকারীর টাইপ করা কিছুই বিশ্বাস করবেন না।
লাইন পড়া। readline-এর async iterator একবারে এক লাইন পড়ে, এবং উত্তর pipe করে দিলেও কাজ করে, যা স্বয়ংক্রিয় যাচাইয়ে কাজে লাগে।
ধাপ ৩: test (quiz.test.mjs)
import test from "node:test";
import assert from "node:assert/strict";
import { questions, shuffle, grade, isCorrect } from "./quiz.mjs";
test("isCorrect compares the chosen index with the answer", () => {
assert.equal(isCorrect(questions[0], 2), true);
assert.equal(isCorrect(questions[0], 1), false);
});
test("grade counts correct picks and builds a message", () => {
assert.deepEqual(grade(questions, [2, 0, 1]), { score: 3, total: 3, percent: 100, message: "Perfect!" });
assert.equal(grade(questions, [2, 0, 0]).percent, 67);
assert.equal(grade(questions, [0, 1, 0]).message, "Keep practising.");
});
test("grade handles an empty quiz", () => {
assert.equal(grade([], []).percent, 0);
});
test("shuffle keeps every item and does not change the original", () => {
const original = [1, 2, 3, 4, 5];
const result = shuffle(original);
assert.deepEqual([...result].sort(), original);
assert.deepEqual(original, [1, 2, 3, 4, 5]);
});
test("shuffle is predictable with a fixed random function", () => {
assert.deepEqual(shuffle([1, 2, 3], () => 0), [2, 3, 1]);
});
$ node --test
✔ isCorrect compares the chosen index with the answer
✔ grade counts correct picks and builds a message
✔ grade handles an empty quiz
✔ shuffle keeps every item and does not change the original
✔ shuffle is predictable with a fixed random function
# tests 5
# pass 5
# fail 0
ওয়েব পাতায় একই logic ব্যবহার
quiz.mjs সাধারণ ES module, তাই ওয়েব পাতা সরাসরি import { grade, shuffle } from "./quiz.mjs" করতে পারে। পাতাকে শুধু প্রশ্ন আঁকতে ও click নিতে হবে। নম্বর গণনার কোড বদলায় না।
জানা সীমা
- উত্তর সোর্স কোডে আছে, তাই আসল পরীক্ষার জন্য উপযোগী নয়। কোড পড়তে পারলে উত্তরও পড়া যায়।
- প্রশ্ন ফাইলে স্থির। বড় quiz-এর জন্য JSON থেকে লোড করুন।
- শুধু একটি সঠিক উত্তরের প্রশ্ন, timer বা সংরক্ষিত সর্বোচ্চ স্কোর নেই।
চ্যালেঞ্জ
১. প্রশ্ন questions.json-এ সরিয়ে fs.readFile দিয়ে লোড করুন।
২. প্রতি প্রশ্নে ১৫ সেকেন্ডের timer যোগ করুন।
৩. কোন প্রশ্ন ভুল হলো তা সঠিক উত্তরসহ দেখান।
৪. সেরা স্কোর ফাইলে সংরক্ষণ করুন।
৫. HTML ও quiz.mjs import করা কয়েক লাইন JavaScript দিয়ে ওয়েব সংস্করণ বানান।
মূল কথা: নিয়ম ছোট pure function-এ রাখুন, ইনপুট-আউটপুট প্রান্তে রাখুন, আর নিয়মগুলো test করুন। এই একটি অভ্যাসই প্রজেক্ট বদলানো, পুনর্ব্যবহার ও ভরসা করা সহজ করে।
হাতে-কলমে প্রজেক্ট সিরিজ, পর্ব ১ (ছোট পরীক্ষিত প্রজেক্ট): 5 নম্বর (মোট 5)। আগের লেখা: Python ও SQLite: CRUD সহ to-do অ্যাপ বানান (পরীক্ষিত).


