পাঠ ৪৮ · ৫৮-এর মধ্যে · মডিউল ১১
Home / Courses / Full-Stack Web Frameworks / ইউনিট টেস্টিং

কম্পোনেন্ট ও ফাংশনের ইউনিট টেস্টিং

Unit testing components and functions
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ইউনিট টেস্ট কী এবং কেন খাঁটি ফাংশন ইউনিট-টেস্ট করা সবচেয়ে সহজ ও নির্ভরযোগ্য
  • একটি ছোট্ট, স্বনির্ভর cart_reducer(state, action) ফাংশন লেখা — ADD_ITEM ও REMOVE_ITEM অ্যাকশন হ্যান্ডল করে
  • L47-এর টেস্ট রানার পুনর্গঠন করে একাধিক concrete assert-ভিত্তিক টেস্ট কেস লেখা
  • রিডিউসারের ইমিউটেবিলিটি (পুরনো state মিউটেট না হওয়া) সত্যিকারের কোড দিয়ে যাচাই করা

১ · ইউনিট টেস্ট কী, এবং কেন খাঁটি ফাংশন সবচেয়ে সহজ লক্ষ্য

ইউনিট টেস্টUnit Testএকটি সবচেয়ে ছোট, বিচ্ছিন্ন কোড-এককের (ফাংশন/ক্লাস) সঠিকতা যাচাই করার টেস্ট, বাইরের কোনো নির্ভরতা ছাড়াই। L47-এর টেস্টিং পিরামিডের সবচেয়ে চওড়া (নিচের) স্তর — অর্থাৎ সবচেয়ে বেশি সংখ্যায় লেখা হয়। একটি ইউনিট টেস্ট কোনো ডেটাবেস, নেটওয়ার্ক কল, বা অন্য কোনো ফাংশনের সঠিক আচরণের উপর নির্ভর করে না — শুধু একটি ফাংশন/ক্লাস, বিচ্ছিন্নভাবে।

এই কারণেই খাঁটি (pure) ফাংশন — যা একই ইনপুটে সবসময় একই আউটপুট দেয়, কোনো বাইরের ভ্যারিয়েবল পড়ে/পরিবর্তন করে না — ইউনিট টেস্ট করা সবচেয়ে সহজ। M4-এ শেখা রিডিউসার প্যাটার্ন (reducer(state, action) -> new_state) ঠিক এই ধরনেরই একটি খাঁটি ফাংশন — তাই এটি ইউনিট টেস্টিং শেখানোর জন্য একটি আদর্শ উদাহরণ।

খাঁটি ফাংশন (সহজে টেস্টযোগ্য)
একই ইনপুট → সবসময় একই আউটপুট, কোনো side effect নেই। শুধু ইনপুট-আউটপুট যাচাই করলেই যথেষ্ট।
অ-খাঁটি ফাংশন (কঠিন)
বর্তমান সময়, র‍্যান্ডম মান, গ্লোবাল ভ্যারিয়েবল বা বাইরের সিস্টেমের উপর নির্ভর করে — ফলাফল ইনপুট দেখে পুরোপুরি অনুমান করা যায় না।

২ · এই পাঠের বিষয়বস্তু — একটি স্বনির্ভর cart_reducer

নিচে M4-এর রিডিউসার প্যাটার্নের আদলে, কিন্তু এই লেসনের নিজস্ব, সম্পূর্ণ স্বনির্ভর একটি ছোট্ট cart_reducer(state, action) ফাংশন লেখা হচ্ছে — একটি শপিং কার্টের state ম্যানেজ করে। state সবসময় {"items": {item_id: {"name": ..., "qty": ...}}} আকৃতির একটি dict। দুটো অ্যাকশন টাইপ হ্যান্ডল করা হবে — ADD_ITEM (নতুন আইটেম যোগ, বা বিদ্যমান আইটেমের qty বাড়ানো) এবং REMOVE_ITEM (আইটেম মুছে ফেলা)। এরপর L47-এর টেস্ট রানারের একটি সরলীকৃত সংস্করণ পুনর্গঠন করে এই রিডিউসারের বিরুদ্ধে একাধিক টেস্ট কেস চালানো হবে।

Python
# ---------- খাঁটি ফাংশন: cart_reducer (M4-স্টাইল, এই পাঠের জন্য স্বনির্ভর) ----------
def cart_reducer(state, action):
    if action["type"] == "ADD_ITEM":
        item_id = action["item_id"]
        name = action["name"]
        qty = action.get("qty", 1)
        new_items = dict(state["items"])
        if item_id in new_items:
            existing = new_items[item_id]
            new_items[item_id] = {"name": existing["name"], "qty": existing["qty"] + qty}
        else:
            new_items[item_id] = {"name": name, "qty": qty}
        return {"items": new_items}

    if action["type"] == "REMOVE_ITEM":
        item_id = action["item_id"]
        new_items = dict(state["items"])
        new_items.pop(item_id, None)
        return {"items": new_items}

    return state   # অজানা action -- state অপরিবর্তিত ফেরত


# ---------- CORE PATTERN 9 (L47): ছোট্ট টেস্ট রানার, এই পাঠের জন্য পুনর্গঠিত ----------
_tests = []

def test(name):
    def decorator(fn):
        _tests.append((name, fn))
        return fn
    return decorator

def run_all():
    passed, failed = 0, 0
    for name, fn in _tests:
        try:
            fn()
            print(f"[PASS] {name}")
            passed += 1
        except AssertionError as e:
            print(f"[FAIL] {name} -- {e}")
            failed += 1
    print(f"\nমোট: {len(_tests)}, পাস: {passed}, ফেইল: {failed}")
    return passed, failed


# ---------- cart_reducer-এর বিরুদ্ধে concrete টেস্ট কেস ----------
@test("খালি কার্টে ADD_ITEM করলে নতুন এন্ট্রি যোগ হয়")
def _():
    state0 = {"items": {}}
    state1 = cart_reducer(state0, {"type": "ADD_ITEM", "item_id": "p1", "name": "Keyboard", "qty": 1})
    assert state1["items"]["p1"] == {"name": "Keyboard", "qty": 1}

@test("একই আইটেম দ্বিতীয়বার ADD_ITEM করলে qty যোগ হয়, নতুন এন্ট্রি হয় না")
def _():
    state0 = {"items": {"p1": {"name": "Keyboard", "qty": 1}}}
    state1 = cart_reducer(state0, {"type": "ADD_ITEM", "item_id": "p1", "name": "Keyboard", "qty": 2})
    assert state1["items"]["p1"]["qty"] == 3
    assert len(state1["items"]) == 1

@test("REMOVE_ITEM নির্দিষ্ট আইটেমটিই মুছে ফেলে, বাকিগুলো রাখে")
def _():
    state0 = {"items": {
        "p1": {"name": "Keyboard", "qty": 3},
        "p2": {"name": "Mouse", "qty": 1},
    }}
    state1 = cart_reducer(state0, {"type": "REMOVE_ITEM", "item_id": "p1"})
    assert "p1" not in state1["items"]
    assert "p2" in state1["items"]

@test("রিডিউসার পুরনো state মিউটেট করে না -- ইমিউটেবিলিটি")
def _():
    state0 = {"items": {"p1": {"name": "Keyboard", "qty": 1}}}
    state1 = cart_reducer(state0, {"type": "ADD_ITEM", "item_id": "p1", "name": "Keyboard", "qty": 1})
    assert state0["items"]["p1"]["qty"] == 1   # পুরনো state অপরিবর্তিত
    assert state1 is not state0                # নতুন একটি আলাদা object

@test("অজানা action type হলে state হুবহু অপরিবর্তিত ফেরত আসে")
def _():
    state0 = {"items": {"p1": {"name": "Keyboard", "qty": 1}}}
    state1 = cart_reducer(state0, {"type": "UNKNOWN_ACTION"})
    assert state1 == state0

run_all()

    
চতুর্থ টেস্টটি (ইমিউটেবিলিটি) গুরুত্বপূর্ণ কেন লক্ষ্য করুন: cart_reducer-এ new_items = dict(state["items"]) লাইনটি একটি নতুন টপ-লেভেল dict তৈরি করে, আর new_items[item_id] = {...}-এ একটি সম্পূর্ণ নতুন আইটেম-dict বসানো হয় (বিদ্যমানটি পরিবর্তন করা হয় না) — তাই state0 অপরিবর্তিত থাকে এবং state1 is not state0 সত্যি হয়। এটিই M4-এর রিডিউসার প্যাটার্নের মূল নিয়ম — সবসময় নতুন state ফেরত দাও, পুরনোটি কখনো মিউটেট কোরো না।
মূল কথা · Key takeaway

ইউনিট টেস্ট সবচেয়ে কার্যকর হয় যখন যা টেস্ট করা হচ্ছে তা খাঁটি ও বিচ্ছিন্ন — cart_reducer কোনো ডেটাবেস বা নেটওয়ার্ক ছোঁয় না, তাই এর প্রতিটি টেস্ট মিলিসেকেন্ডে চলে এবং ফলাফল সম্পূর্ণ নির্ভরযোগ্য। L47-এর পিরামিডে এই স্তরটিই সবচেয়ে চওড়া হওয়ার কারণ এখানেই স্পষ্ট — কম খরচে বেশি আত্মবিশ্বাস। পরের পাঠ (L49) দেখাবে যখন একাধিক কম্পোনেন্ট একসাথে কাজ করা যাচাই করতে হয়, তখন ইন্টিগ্রেশন টেস্ট কীভাবে লেখা হয়।

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

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

প্র ০১ যদি cart_reducer-এর ভেতরে সরাসরি state["items"][item_id]["qty"] += qty লেখা হতো (নতুন dict তৈরি না করে), তাহলে কোন টেস্টটি ফেইল করত এবং কেন?

"ইমিউটেবিলিটি" টেস্টটি ফেইল করত। কারণ state["items"][item_id]["qty"] += qty সরাসরি পুরনো state0-এর ভেতরের dict-টিকেই পরিবর্তন করে দিত (in-place মিউটেশন) — তাই state0["items"]["p1"]["qty"] == 1 আর সত্যি থাকত না (এটি বেড়ে ২ হয়ে যেত), assert ব্যর্থ হয়ে AssertionError তুলত এবং রানার সেটিকে [FAIL] হিসেবে রিপোর্ট করত।

প্র ০২ প্রতিটি টেস্ট ফাংশনের ভেতরে নতুন করে state0 = {...} লেখা হয়েছে কেন — সবগুলো টেস্টে একটি শেয়ার্ড state0 ব্যবহার করলে সমস্যা কী হতো?

একটি ভালো ইউনিট টেস্ট বিচ্ছিন্ন (isolated) হওয়া উচিত — একটি টেস্ট আরেকটির ফলাফলের উপর নির্ভর করবে না। যদি একটি শেয়ার্ড state0 একাধিক টেস্টে ব্যবহৃত হতো এবং কোনো একটি টেস্ট ভুলবশত সেটি মিউটেট করে ফেলত, তাহলে পরবর্তী টেস্টগুলো ভুল ইনপুট নিয়ে চলত এবং কোন টেস্টের কারণে আসল সমস্যা হলো তা বোঝা কঠিন হয়ে যেত। প্রতিটি টেস্টে নতুন করে state তৈরি করা এই নির্ভরতা এড়ায়।

প্র ০৩ উপরের কোডে সব টেস্ট আসলে পাস করে কি না তা কীভাবে নিশ্চিতভাবে বলা যায় (কোড না চালিয়েই)?

প্রতিটি টেস্টের যুক্তি হাতে-কলমে ট্রেস করে — যেমন দ্বিতীয় টেস্টে existing["qty"] (১) + qty (২) = ৩, আর assert ঠিক == 3 চেক করছে, তাই এটি সত্যি। এভাবে প্রতিটি assert-এর দুই পাশ হাতে হিসাব করে মিলিয়ে দেখাই "হ্যান্ড-ট্রেসিং" — বাস্তব কোড লেখার সময় এটি একটি গুরুত্বপূর্ণ অভ্যাস, রান করার আগেও যুক্তির সঠিকতা যাচাইয়ের জন্য।

অনুশীলন

  1. চিন্তা করুন: cart_reducer-এ একটি নতুন অ্যাকশন টাইপ CLEAR_CART (সব আইটেম মুছে খালি কার্ট ফেরত) যোগ করতে হলে, সেটির জন্য কোন কোন টেস্ট কেস লেখা প্রয়োজন হবে বলে মনে করেন?

    অন্তত দুটো — (১) একাধিক আইটেমসহ একটি কার্টে CLEAR_CART চালালে state1["items"] খালি dict ({}) হওয়া নিশ্চিত করা, এবং (২) ইমিউটেবিলিটি — state0 অপরিবর্তিত থাকছে কি না তা যাচাই করা, ঠিক যেমন এই পাঠের চতুর্থ টেস্টে ADD_ITEM-এর জন্য করা হয়েছে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন টেস্ট যোগ করুন যা যাচাই করে — REMOVE_ITEM দিয়ে এমন একটি item_id মুছতে চাইলে যা কার্টে নেই, তাহলে state1["items"] আগের মতোই থাকে (কোনো এরর ছাড়া)। Run চেপে দেখুন এটি পাস করে কি না।

    এটি পাস করবে, কারণ new_items.pop(item_id, None)-এ দ্বিতীয় আর্গুমেন্ট None থাকায় item_id dict-এ না থাকলেও কোনো KeyError ওঠে না — pop() শুধু চুপচাপ None ফেরত দেয় এবং dict অপরিবর্তিত থাকে। এটি দেখায় কেন .pop(key, default) ব্যবহার করা del dict[key]-এর চেয়ে নিরাপদ, যখন key না থাকার সম্ভাবনা থাকে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Python Programming কোর্স সহোদর কোর্স এই পাঠের dict কপি করা, ডেকোরেটর ও ক্লোজারের ভাষাগত ভিত্তি সেই কোর্সে তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
ফুল-স্ট্যাক অ্যাপের জন্য টেস্টিং পিরামিড