JavaScript quiz বানান: logic আলাদা করে test করুন

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

JavaScript quiz চালিয়ে শেষে স্কোর দেখানো একটি 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 অ্যাপ বানান (পরীক্ষিত).

আরও পড়ুন

Python দিয়ে এলোমেলো CSV পরিষ্কার: নাম, তারিখ, ফোন ও ডুপ্লিকেট

আসল data এলোমেলো। standard library দিয়ে contacts পরিষ্কার করা, প্রতিটি বাতিল row ব্যাখ্যা করা ও test-সহ একটি script বানান।

প্রজেক্ট•৫ মিনিটে পড়া