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

এন্ড-টু-এন্ড টেস্টিং স্ট্র্যাটেজি

End-to-end testing strategies
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • E2E টেস্ট কীভাবে ইউনিট (L48) ও ইন্টিগ্রেশন (L49) টেস্ট থেকে ভিন্ন — সম্পূর্ণ জার্নি বনাম আংশিক প্রবাহ
  • কয়েকটি ছোট্ট, স্বনির্ভর সিস্টেম (auth, resource-creation, listing) একসাথে চেইন করে একটি বাস্তব দৃশ্য তৈরি করা
  • L47-এর টেস্ট রানার দিয়ে সম্পূর্ণ জার্নির প্রতিটি ধাপ assert করে যাচাই ও ট্রেস করা
  • একটি নেগেটিভ-পাথ (টোকেন ছাড়া) কেসও একই জার্নি সিস্টেমের বিরুদ্ধে টেস্ট করা

১ · E2E টেস্ট কেন আলাদা — "জার্নি" বনাম "ধাপ"

এন্ড-টু-এন্ড টেস্টEnd-to-End (E2E) Testএকটি সম্পূর্ণ ব্যবহারকারী-জার্নি শুরু থেকে শেষ পর্যন্ত, পুরো সিস্টেম দিয়ে বাস্তবের মতো চালিয়ে যাচাই করা টেস্ট। L49-এর ইন্টিগ্রেশন টেস্ট একটি একক প্রবাহ (create → read → update → delete, একটিই resource নিয়ে) যাচাই করেছিল। E2E টেস্ট আরও বড় — এটি একটি সম্পূর্ণ ব্যবহারকারী-জার্নি অনুকরণ করে, যেখানে একাধিক আলাদা সিস্টেম (অথেন্টিকেশন, রিসোর্স তৈরি, লিস্টিং) একের পর এক ব্যবহৃত হয় — ঠিক যেমন একজন প্রকৃত ব্যবহারকারী অ্যাপ ব্যবহার করার সময় করত।

ধাপ ১ — লগইন
ব্যবহারকারী ইউজারনেম/পাসওয়ার্ড দেয়, সিস্টেম একটি টোকেন ফেরত দেয় (M8-এর টোকেন-ভিত্তিক অথের প্রিভিউ)।
ধাপ ২ — রিসোর্স তৈরি
সেই টোকেন দিয়ে একটি নতুন রিসোর্স তৈরির অনুরোধ করা হয় (M6-এর CRUD প্যাটার্নের প্রিভিউ) — টোকেন ছাড়া এই ধাপ ব্যর্থ হবে।
ধাপ ৩ — লিস্টে যাচাই
তৈরি করা রিসোর্সটি সত্যিই লিস্ট এন্ডপয়েন্টে ফেরত আসছে কি না নিশ্চিত করা হয় — পুরো জার্নির চূড়ান্ত প্রমাণ।

২ · তিনটি স্বনির্ভর স্ট্যান্ড-ইন সিস্টেম ও একটি সম্পূর্ণ জার্নি টেস্ট

নিচের কোড সেলে তিনটি ছোট্ট, সম্পূর্ণ স্বনির্ভর ফাংশন-সেট তৈরি করা হচ্ছে — একটি সরল অথ সিস্টেম, একটি টোকেন-সুরক্ষিত রিসোর্স-তৈরি ফাংশন, আর একটি লিস্টিং ফাংশন। এরপর L47-এর টেস্ট রানার পুনর্গঠন করে পুরো জার্নিটি একটি একক টেস্ট ফাংশনের ভেতরে ধাপে ধাপে চালানো হচ্ছে, প্রতিটি ধাপের ট্রেস প্রিন্ট করে এবং assert দিয়ে যাচাই করে।

Python
# ---------- 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


# ---------- সিস্টেম ১: সরল টোকেন-ভিত্তিক অথ (M8-স্টাইল, স্বনির্ভর সংস্করণ) ----------
_users = {"rina": "correct-password"}
_valid_tokens = set()
_token_counter = 0

def login(username, password):
    if _users.get(username) != password:
        return {"ok": False, "token": None, "error": "ভুল ইউজারনেম/পাসওয়ার্ড"}
    global _token_counter
    _token_counter += 1
    token = f"tok-{_token_counter}"
    _valid_tokens.add(token)
    return {"ok": True, "token": token, "error": None}

def is_authenticated(token):
    return token in _valid_tokens


# ---------- সিস্টেম ২: টোকেন-সুরক্ষিত রিসোর্স তৈরি (M6-স্টাইল CRUD, স্বনির্ভর সংস্করণ) ----------
_resources = {}
_next_resource_id = 1

def create_resource(token, title):
    if not is_authenticated(token):
        return {"status": 401, "body": None}
    global _next_resource_id
    resource = {"id": _next_resource_id, "title": title}
    _resources[_next_resource_id] = resource
    _next_resource_id += 1
    return {"status": 201, "body": resource}


# ---------- সিস্টেম ৩: রিসোর্স লিস্টিং (M6-স্টাইল, স্বনির্ভর সংস্করণ) ----------
def list_resources(token):
    if not is_authenticated(token):
        return {"status": 401, "body": None}
    return {"status": 200, "body": list(_resources.values())}


# ---------- সম্পূর্ণ ব্যবহারকারী-জার্নি: লগইন -> রিসোর্স তৈরি -> লিস্টে যাচাই ----------
@test("সম্পূর্ণ ব্যবহারকারী-জার্নি -- লগইন -> রিসোর্স তৈরি -> লিস্টে যাচাই")
def _():
    print("  ধাপ ১: লগইন করা হচ্ছে (rina)...")
    login_resp = login("rina", "correct-password")
    assert login_resp["ok"] is True
    token = login_resp["token"]
    print(f"    -> টোকেন পাওয়া গেছে: {token}")

    print("  ধাপ ২: টোকেন দিয়ে একটি নতুন রিসোর্স তৈরি করা হচ্ছে...")
    create_resp = create_resource(token, "আমার প্রথম পোস্ট")
    assert create_resp["status"] == 201
    new_id = create_resp["body"]["id"]
    print(f"    -> রিসোর্স তৈরি হয়েছে, id={new_id}")

    print("  ধাপ ৩: লিস্ট এন্ডপয়েন্ট কল করে যাচাই করা হচ্ছে নতুন রিসোর্সটি আছে কি না...")
    list_resp = list_resources(token)
    assert list_resp["status"] == 200
    ids_in_list = [r["id"] for r in list_resp["body"]]
    assert new_id in ids_in_list
    print(f"    -> লিস্টে পাওয়া আইডি-সমূহ: {ids_in_list}")


# ---------- নেগেটিভ-পাথ: টোকেন ছাড়া রিসোর্স তৈরির চেষ্টা ----------
@test("নেগেটিভ পাথ -- টোকেন ছাড়া রিসোর্স তৈরির চেষ্টা করলে 401 ফেরত আসে")
def _():
    resp = create_resource("fake-token-that-does-not-exist", "অননুমোদিত পোস্ট")
    assert resp["status"] == 401
    assert resp["body"] is None


print("== এন্ড-টু-এন্ড ব্যবহারকারী-জার্নি টেস্ট ==")
run_all()

    
লক্ষ্য করুন প্রথম টেস্টে token ভ্যারিয়েবলটি ধাপ ১-এ পাওয়া গিয়ে ধাপ ২ ও ৩-এ পুনরায় ব্যবহৃত হয়েছে — ঠিক যেমন একজন প্রকৃত ব্যবহারকারী একবার লগইন করে সেই সেশন দিয়ে একাধিক কাজ করে। যদি ধাপ ১ ব্যর্থ হতো (ভুল পাসওয়ার্ড), তাহলে token কখনো পাওয়া যেত না এবং পুরো টেস্ট প্রথম assert-এই থেমে যেত — ধাপ ২ ও ৩ আর চলতই না। এটাই E2E টেস্টের একটি বৈশিষ্ট্য — ধাপগুলো একে অপরের উপর নির্ভরশীল, ঠিক বাস্তব ব্যবহারকারীর জার্নির মতোই।
মূল কথা · Key takeaway

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

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

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

প্র ০১ যদি login() সফল হওয়ার পরও কেউ ভুলবশত create_resource()-এ token-এর বদলে খালি স্ট্রিং "" পাঠাত, তাহলে কী হতো?

is_authenticated("") চেক করত "" in _valid_tokens — যেহেতু _valid_tokens-এ কখনো খালি স্ট্রিং যোগ করা হয়নি, এটি False ফেরত দিত। তাই create_resource সরাসরি {"status": 401, "body": None} ফেরত দিত, এবং পরের assert create_resp["status"] == 201 ব্যর্থ হয়ে টেস্টটি সেখানেই [FAIL] হিসেবে থেমে যেত।

প্র ০২ এই পাঠের E2E টেস্ট এবং L49-এর ইন্টিগ্রেশন টেস্টের মধ্যে সবচেয়ে বড় কাঠামোগত পার্থক্য কী?

L49-এর ইন্টিগ্রেশন টেস্ট একটি একক সিস্টেমের (একটি Api ক্লাসের) ভেতরের একাধিক অপারেশন (create/read/update/delete) পরীক্ষা করেছিল। এই পাঠের E2E টেস্ট তিনটি আলাদা সিস্টেম (অথ, রিসোর্স-তৈরি, লিস্টিং) একসাথে চেইন করেছে, যেখানে একটি সিস্টেমের আউটপুট (টোকেন) পরের সিস্টেমের ইনপুট হিসেবে ব্যবহৃত হয়েছে — এটিই সম্পূর্ণ জার্নি, শুধু একটি সিস্টেমের অভ্যন্তরীণ আচরণ নয়।

প্র ০৩ দ্বিতীয় (নেগেটিভ-পাথ) টেস্টে resp["body"] is None কেন সত্যি হয়?

কারণ create_resource()-এর ভেতরে is_authenticated(token) মিথ্যা হলে ফাংশনটি সরাসরি return {"status": 401, "body": None} রিটার্ন করে — কোনো রিসোর্স তৈরির চেষ্টাই করা হয় না, তাই body সবসময় আক্ষরিকভাবে None। যেহেতু "fake-token-that-does-not-exist" কখনো login() থেকে আসেনি, এটি _valid_tokens সেটে নেই, তাই is_authenticated এখানে False ফেরত দেয়।

অনুশীলন

  1. চিন্তা করুন: এই তিন-ধাপের জার্নিতে একটি চতুর্থ ধাপ ("ধাপ ৪: লগআউট করে, একই টোকেন দিয়ে আবার লিস্ট কল করলে 401 আসা উচিত") যোগ করতে হলে, কোন সিস্টেমে কী পরিবর্তন আনতে হবে?

    একটি logout(token) ফাংশন যোগ করতে হবে যা _valid_tokens.discard(token) করে টোকেনটি বাতিল করে দেয়। এরপর টেস্টে logout(token) কল করার পর list_resources(token) আবার কল করলে is_authenticated(token) এখন False ফেরত দেবে (যেহেতু টোকেন সেট থেকে সরিয়ে ফেলা হয়েছে), তাই status হবে ৪০১ — ঠিক এই পাঠের নেগেটিভ-পাথ টেস্টের মতোই একই যুক্তিতে।

  2. পরীক্ষা করুন: উপরের কোড সেলে login("rina", "wrong-password") দিয়ে একটি নতুন টেস্ট যোগ করুন যা যাচাই করে login_resp["ok"] is False এবং login_resp["token"] is None, তারপর Run চেপে দেখুন এটি পাস করে কি না।

    পাস করবে। login()-এ _users.get("rina") != "wrong-password" সত্যি হবে (কারণ আসল পাসওয়ার্ড "correct-password"), তাই ফাংশনটি সরাসরি {"ok": False, "token": None, "error": "..."} রিটার্ন করে — কোনো টোকেন তৈরি হয় না, _token_counter বাড়ে না, এবং _valid_tokens-এও কিছু যোগ হয় না।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Python Programming কোর্স সহোদর কোর্স এই পাঠের গ্লোবাল স্টেট ও সেট (set) ব্যবহারের ভাষাগত ভিত্তি সেই কোর্সে তৈরি হয়েছে।
  • সব 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 — সব এক জায়গায়।
আগের পাঠ
API-র ইন্টিগ্রেশন টেস্টিং