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

ফুল-স্ট্যাক অ্যাপের জন্য টেস্টিং পিরামিড

The testing pyramid for full-stack apps
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টেস্টিং পিরামিডের তিনটি স্তর এবং প্রতিটির বৈশিষ্ট্য (গতি, বিচ্ছিন্নতা, কভারেজের ধরন)
  • কেন স্তরের আকার নিচ থেকে উপরে কমতে থাকা উচিত — গতি ও নির্ভরযোগ্যতার ট্রেড-অফ
  • M11 জুড়ে ব্যবহৃত একটি ছোট্ট, সত্যিকারের Python টেস্ট রানার (test() ডেকোরেটর + run_all())
  • বাস্তব পার্-টেস্ট সময় দিয়ে হাতে-কলমে গণনা — সঠিক পিরামিড বনাম উল্টানো পিরামিডের মোট সিমুলেটেড সময়

১ · টেস্টিং পিরামিড কী

একটি ফুল-স্ট্যাক অ্যাপে অনেক ধরনের টেস্ট লেখা যায় — কিন্তু সবগুলো টেস্ট সমান নয়। কিছু টেস্ট একটি ছোট্ট, বিচ্ছিন্ন ফাংশন যাচাই করে (মিলিসেকেন্ডে শেষ), কিছু টেস্ট একাধিক অংশ একসাথে কাজ করছে কি না যাচাই করে (কিছুটা ধীর), আর কিছু টেস্ট পুরো অ্যাপ্লিকেশন — ফ্রন্ট-এন্ড থেকে ডেটাবেস পর্যন্ত — বাস্তবের মতো চালিয়ে যাচাই করে (সবচেয়ে ধীর, সবচেয়ে ভঙ্গুর)। টেস্টিং পিরামিডTesting Pyramidএকটি নির্দেশিকা যা বলে দেয় প্রতিটি স্তরের টেস্ট কী অনুপাতে থাকা উচিত — নিচে বেশি, উপরে কম। এই তিন ধরনের টেস্টকে একটি অনুপাতে সংগঠিত করার একটি প্রমাণিত নির্দেশিকা।

E2E Integration Unit অনেক · দ্রুত · আইসোলেটেড ২টি — সবচেয়ে ধীর (~২০০ms/টি) ৫টি — মাঝারি (~২০ms/টি) ২০টি — সবচেয়ে দ্রুত (~১ms/টি)
নিচের স্তরে সবচেয়ে বেশি (এবং সবচেয়ে সস্তা) টেস্ট, উপরের স্তরে সবচেয়ে কম (এবং সবচেয়ে ব্যয়বহুল) টেস্ট — নিচের কোড সেলে এই অনুপাতেরই বাস্তব সময় গণনা করা হবে।

২ · প্রতিটি স্তরের বৈশিষ্ট্য

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

৩ · একটি ছোট্ট টেস্ট রানার ও বাস্তব সংখ্যা দিয়ে যাচাই

নিচের কোড সেলে প্রথমে M11 জুড়ে ব্যবহৃত হবে এমন একটি ছোট্ট, সত্যিকারের টেস্ট রানার তৈরি করা হচ্ছে — test() ডেকোরেটর প্রতিটি টেস্ট ফাংশনকে (তার "ধরন" ও সিমুলেটেড সময়সহ) একটি তালিকায় জমা করে, আর run_all() প্রতিটি চালিয়ে AssertionError ধরে পাস/ফেইল গোনে। এরপর দুটো স্যুট চালানো হচ্ছে — একটি স্বাস্থ্যকর পিরামিড-অনুপাতে (২০ ইউনিট / ৫ ইন্টিগ্রেশন / ২ E2E), আরেকটি ঠিক উল্টো অনুপাতে (২ ইউনিট / ৫ ইন্টিগ্রেশন / ২০ E2E) — একই পার্-টেস্ট সময় ব্যবহার করে, যাতে শুধু অনুপাতের প্রভাবটাই দেখা যায়।

Python
# ---------- CORE PATTERN 9: ছোট্ট টেস্ট রানার (M11 জুড়ে ব্যবহৃত হবে) ----------
def make_registry():
    return []

def test(registry, name, kind, duration_ms):
    """ডেকোরেটর -- টেস্ট ফাংশনকে তার ধরন ও সিমুলেটেড সময়সহ রেজিস্ট্রিতে জমা করে"""
    def decorator(fn):
        registry.append({"name": name, "kind": kind, "duration_ms": duration_ms, "fn": fn})
        return fn
    return decorator

def run_all(registry, label):
    passed, failed = 0, 0
    total_ms = 0
    by_kind_ms = {}
    by_kind_count = {}
    for t in registry:
        try:
            t["fn"]()
            status = "PASS"
            passed += 1
        except AssertionError:
            status = "FAIL"
            failed += 1
        total_ms += t["duration_ms"]
        by_kind_ms[t["kind"]] = by_kind_ms.get(t["kind"], 0) + t["duration_ms"]
        by_kind_count[t["kind"]] = by_kind_count.get(t["kind"], 0) + 1
    print(f"== {label} ==")
    for kind in ("unit", "integration", "e2e"):
        if kind in by_kind_count:
            print(f"  {kind:11s} : {by_kind_count[kind]:2d}টি টেস্ট, মোট {by_kind_ms[kind]:5d}ms")
    print(f"  পাস: {passed}, ফেইল: {failed}, সর্বমোট সিমুলেটেড সময়: {total_ms}ms\n")
    return total_ms


# ---------- স্যুট ১: স্বাস্থ্যকর পিরামিড (২০ ইউনিট / ৫ ইন্টিগ্রেশন / ২ E2E) ----------
pyramid = make_registry()

for i in range(20):
    @test(pyramid, f"unit_check_{i+1}", "unit", 1)
    def _(i=i):
        assert (i + 1) > 0   # সত্যিকারের, দ্রুত, বিচ্ছিন্ন যাচাই

for i in range(5):
    @test(pyramid, f"integration_check_{i+1}", "integration", 20)
    def _(i=i):
        assert (i + 1) * 2 == i * 2 + 2   # একাধিক ধাপের মিথস্ক্রিয়ার সরল স্ট্যান্ড-ইন

for i in range(2):
    @test(pyramid, f"e2e_check_{i+1}", "e2e", 200)
    def _(i=i):
        assert (i + 1) >= 1   # সম্পূর্ণ-সিস্টেম যাচাইয়ের সরল স্ট্যান্ড-ইন

pyramid_total = run_all(pyramid, "স্যুট ১ -- সঠিক পিরামিড (২০/৫/২)")


# ---------- স্যুট ২: উল্টানো পিরামিড (২ ইউনিট / ৫ ইন্টিগ্রেশন / ২০ E2E), একই পার্-টেস্ট সময় ----------
inverted = make_registry()

for i in range(2):
    @test(inverted, f"unit_check_{i+1}", "unit", 1)
    def _(i=i):
        assert (i + 1) > 0

for i in range(5):
    @test(inverted, f"integration_check_{i+1}", "integration", 20)
    def _(i=i):
        assert (i + 1) * 2 == i * 2 + 2

for i in range(20):
    @test(inverted, f"e2e_check_{i+1}", "e2e", 200)
    def _(i=i):
        assert (i + 1) >= 1

inverted_total = run_all(inverted, "স্যুট ২ -- উল্টানো পিরামিড (২/৫/২০)")


# ---------- তুলনা ----------
print(f"পিরামিড-আকৃতির স্যুটের মোট সময়:  {pyramid_total}ms")
print(f"উল্টানো পিরামিডের মোট সময়:      {inverted_total}ms")
print(f"পার্থক্য: {inverted_total - pyramid_total}ms বেশি, অর্থাৎ {inverted_total / pyramid_total:.2f}x ধীর -- "
      f"একই মোট টেস্ট-সংখ্যা (২৭টি) হওয়া সত্ত্বেও শুধু অনুপাত পাল্টানোয়!")

    
দুই স্যুটেই মোট টেস্ট সংখ্যা একই (২৭টি — ২০+৫+২ বা ২+৫+২০), এবং প্রতিটি ধরনের পার্-টেস্ট সময়ও একই (ইউনিট ১ms, ইন্টিগ্রেশন ২০ms, E2E ২০০ms) — শুধু কোন ধরনের কতগুলো তা উল্টে গেছে। তাতেই পিরামিড-আকৃতির স্যুট (২০×১ + ৫×২০ + ২×২০০ = ৫২০ms) বনাম উল্টানো পিরামিড (২×১ + ৫×২০ + ২০×২০০ = ৪১০২ms) — প্রায় ৮ গুণ ধীর হয়ে যায়। বাস্তব প্রজেক্টে শত শত টেস্ট নিয়ে এই পার্থক্য মিনিট থেকে ঘণ্টায় গিয়ে দাঁড়ায় — এটাই পিরামিড আকৃতি মেনে চলার মূল ব্যবহারিক কারণ।
মূল কথা · Key takeaway

টেস্টিং পিরামিড শুধু একটি নান্দনিক নিয়ম নয় — এটি একটি গাণিতিক পরিণতি। ধীর টেস্ট (E2E) বেশি রাখলে স্যুটের মোট সময় দ্রুত বেড়ে যায়, কারণ প্রতিটি ধীর টেস্টের "সময়-মূল্য" অনেক বেশি। ইউনিট টেস্টে বেশি বিনিয়োগ করে, ইন্টিগ্রেশনে মাঝারি, আর E2E-তে সবচেয়ে কম (শুধু সবচেয়ে গুরুত্বপূর্ণ জার্নি) — এটাই দ্রুত ও নির্ভরযোগ্য টেস্ট স্যুট পাওয়ার চাবিকাঠি। এই পাঠের test()/run_all() রানারটিই L48-L50-এ পুনর্গঠিত হয়ে প্রতিটি স্তরের গভীরে যাবে।

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

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

প্র ০১ যদি E2E টেস্ট সবচেয়ে বেশি "বাস্তবতার কাছাকাছি" হয়, তাহলে সব টেস্ট E2E বানালে সমস্যা কী?

E2E টেস্ট বাস্তবতার সবচেয়ে কাছাকাছি ঠিকই, কিন্তু তা প্রতিটি টেস্টের জন্য অনেক বেশি সময় ও রিসোর্স লাগে (উপরের কোড সেলে ২০০ms বনাম ১ms — ২০০ গুণ পার্থক্য), এবং ছোট, অসংশ্লিষ্ট কারণেও ফেইল করতে পারে (নেটওয়ার্ক টাইমিং, UI-এর সামান্য পরিবর্তন)। হাজার হাজার E2E টেস্ট চালাতে ঘণ্টা লেগে যেতে পারে এবং কোন টেস্ট আসল বাগ ধরল আর কোনটা শুধু "ফ্লেকি" (flaky) ছিল তা আলাদা করা কঠিন হয়ে পড়ে।

প্র ০২ উপরের কোডে pyramid_total ঠিক কত হওয়া উচিত, এবং কেন?

৫২০ms — কারণ ২০টি ইউনিট টেস্ট × ১ms = ২০ms, ৫টি ইন্টিগ্রেশন টেস্ট × ২০ms = ১০০ms, ২টি E2E টেস্ট × ২০০ms = ৪০০ms; এই তিনটি যোগ করলে ২০ + ১০০ + ৪০০ = ৫২০ms। run_all() ফাংশনটি প্রতিটি রেজিস্টার্ড টেস্টের duration_ms ঠিক এভাবেই যোগ করে total_ms-এ জমা করে।

প্র ০৩ দুই স্যুটেই সব টেস্ট আসলে পাস করে (কোনো বাগ ধরা পড়েনি) — তাহলে এই কোড সেল থেকে কী শেখা গেল?

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

অনুশীলন

  1. চিন্তা করুন: একটি টিম যদি ১০০টি ইউনিট টেস্ট, ১০০টি ইন্টিগ্রেশন টেস্ট, আর ১০০টি E2E টেস্ট রাখে (সমান সংখ্যায়, "সমতাবাদী" স্যুট) — এটি কি স্বাস্থ্যকর পিরামিড, নাকি আরেক ধরনের সমস্যা?

    এটি সমস্যাযুক্ত — একে বলা হয় "টেস্টিং আইসক্রিম কোণ" (বা মাঝখানে ফুলে থাকা আকৃতি) না হলেও, সমান সংখ্যক E2E টেস্ট থাকার মানে হলো উপরের কোড সেলের মতোই — ১০০টি E2E × ২০০ms = ২০,০০০ms শুধু E2E অংশেই, যা মোট সময়ের সিংহভাগ দখল করে নেবে। স্বাস্থ্যকর পিরামিডে E2E সংখ্যা ইউনিট/ইন্টিগ্রেশনের তুলনায় উল্লেখযোগ্যভাবে কম হওয়া উচিত, সমান নয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে pyramid স্যুটে E2E টেস্টের সংখ্যা ২ থেকে বাড়িয়ে ১০ করুন (range(2) থেকে range(10)), Run চেপে দেখুন pyramid_total কত বেড়ে যায় এবং সেটি কি এখনও inverted_total-এর চেয়ে কম থাকে।

    E2E ২টি থেকে ১০টি হলে সেই অংশের সময় ৪০০ms থেকে ২০০০ms হবে, তাই নতুন pyramid_total = ২০ + ১০০ + ২০০০ = ২১২০ms। এটি এখনও inverted_total (৪১০২ms)-এর চেয়ে কম, কিন্তু আগের ৫২০ms-এর তুলনায় অনেক বেশি — দেখায় E2E সংখ্যা বাড়লে খুব দ্রুতই মোট সময়ের উপর প্রভাব পড়ে, কারণ প্রতিটি E2E টেস্টের "সময়-মূল্য" এত বেশি।

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

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