পাঠ ৫৪ · ৫৭-এর মধ্যে · মডিউল ১২
Home / Courses / Software Testing & Quality Assurance / CI/CD টেস্টিং

CI/CD পাইপলাইনে কন্টিনিউয়াস টেস্টিং

Continuous testing in CI/CD pipelines
৯ মিনিট পড়া উচ্চ-মধ্যবর্তী · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • একটি টেস্টিং-কেন্দ্রিক CI/CD পাইপলাইনের স্টেজ ক্রম এবং প্রতিটি স্টেজে কোন ধরনের টেস্ট চলে
  • এই কোর্সের ইউনিট (M3), ইন্টিগ্রেশন (M5), ও সিকিউরিটি (M9) টেস্টিং মডিউলগুলো পাইপলাইনে ঠিক কোথায় ফিট করে
  • early-stop-on-failure কীভাবে কাজ করে এবং কেন এটি গুরুত্বপূর্ণ
  • একটি সত্যিকারের Python পাইপলাইন রানার লিখে, কল-কাউন্ট দিয়ে যাচাই করা যে skip করা স্টেজ সত্যিই চালানো হয়নি

১ · পাঁচটি স্টেজ, এবং প্রতিটিতে কোন টেস্ট টাইপ

একটি CI/CD পাইপলাইন কোড পরিবর্তনকে কয়েকটি স্বয়ংক্রিয় স্টেজের মধ্য দিয়ে চালায়, প্রতিটি স্টেজ আগেরটি সফল হলেই চলে। ../full-stack-web-frameworks/-এর CI/CD পাইপলাইন লেসন (lint → test → build → deploy) ও ../mobile-app-development/-এর CI/CD for mobile apps লেসন একই early-stop যান্ত্রিকতা কভার করে সাধারণ ওয়েব/মোবাইল ডিপ্লয়মেন্ট প্রেক্ষাপটে — এই পাঠের ভার্সনটি টেস্টিং কোর্সের জন্য বিশেষায়িত: "test" স্টেজকে ভেঙে দেখানো হয়েছে ঠিক কোন টেস্ট টাইপ কোথায় বসে।

lint unit_test integration_test security_scan deploy স্ট্যাটিক কোড চেক M3 -- ইউনিট টেস্টিং M5 -- ইন্টিগ্রেশন টেস্টিং M9 -- সিকিউরিটি টেস্টিং প্রোডাকশনে প্রকাশ
যেকোনো একটি স্টেজ ব্যর্থ হলে তার ডানদিকের সব স্টেজ সম্পূর্ণ skip হয়ে যায় — কখনো চালানোই হয় না। "test" কে এখানে unit_test, integration_test, security_scan-এ ভেঙে দেখানো হয়েছে, যাতে বোঝা যায় এই কোর্সের প্রতিটি টেস্ট টাইপ পাইপলাইনে ঠিক কোথায় বসে।
lint
কোড স্টাইল ও সাধারণ সিনট্যাক্স ভুল দ্রুত ধরে — সবচেয়ে সস্তা চেক, তাই প্রথমে চলে (M12/L51-এর শিফট-লেফট নীতির সাথে সামঞ্জস্যপূর্ণ)।
unit_test
M3-এ শেখা unittest-ভিত্তিক ইউনিট টেস্ট — ছোট, বিচ্ছিন্ন ফাংশন/ক্লাসের আচরণ যাচাই করে, দ্রুত চলে।
integration_test
M5-এ শেখা ইন্টিগ্রেশন টেস্ট — একাধিক মডিউল/সার্ভিস একসাথে সঠিকভাবে কাজ করছে কি না যাচাই করে, ইউনিট টেস্টের চেয়ে ধীর।
security_scan
M9-এ শেখা সিকিউরিটি টেস্টিং (যেমন SAST-স্টাইল স্ক্যান, M9/L37) — কোড ডিপ্লয় হওয়ার আগে সাধারণ নিরাপত্তা ঝুঁকি খোঁজে।

২ · একটি সত্যিকারের পাইপলাইন রানার — early-stop ও কল-কাউন্ট যাচাই

নিচের কোড সেলে প্রতিটি স্টেজ একটি real ফাংশন — কল হলে নিজের call_counts কাউন্টার বাড়ায়, এবং একটি ইনজেক্ট করা fail_stages সেটে নিজের নাম থাকলে False রিটার্ন করে। run_pipeline() স্টেজগুলো ক্রমানুসারে চালায় — একটি স্টেজ ব্যর্থ হলে পরের সব স্টেজকে "SKIPPED" চিহ্নিত করে দেয়, কিন্তু তাদের ফাংশন কখনো কল করে না। রান ১-এ সব স্টেজ সফল হবে; রান ২-এ integration_test-এ ইচ্ছাকৃতভাবে ব্যর্থতা ইনজেক্ট করা হয়েছে — তারপর security_scan ও deploy-এর call_count সত্যিই 0 কি না তা assert দিয়ে যাচাই করা হয়েছে।

Python
STAGE_ORDER = ["lint", "unit_test", "integration_test", "security_scan", "deploy"]

def make_stage(stage_name, fail_stages, call_counts):
    """একটি স্টেজ ফাংশন তৈরি করে দেয়। কল হলে call_counts[stage_name] বাড়ায় --
    তাই এটি কতবার কল হয়েছে তা পরে যাচাইযোগ্য। fail_stages সেটে stage_name
    থাকলে False (ব্যর্থ), নাহলে True (সফল) রিটার্ন করে।"""
    def stage_fn():
        call_counts[stage_name] += 1
        return stage_name not in fail_stages
    return stage_fn

def run_pipeline(fail_stages):
    """STAGE_ORDER অনুযায়ী প্রতিটি স্টেজ ক্রমানুসারে চালায়। কোনো স্টেজ ব্যর্থ হলে
    সাথে সাথে থেমে যায় -- পরের স্টেজগুলোর ফাংশন একবারও কল করা হয় না, শুধু
    ফলাফলে "SKIPPED" হিসেবে চিহ্নিত করা হয়। রিটার্ন করে: (results, call_counts, failed_at)।"""
    call_counts = {name: 0 for name in STAGE_ORDER}
    stages = {name: make_stage(name, fail_stages, call_counts) for name in STAGE_ORDER}

    results = {}
    failed_at = None
    for name in STAGE_ORDER:
        if failed_at is not None:
            results[name] = "SKIPPED"
            continue                      # এই স্টেজের ফাংশন কখনো কল করা হচ্ছে না
        succeeded = stages[name]()
        results[name] = "SUCCESS" if succeeded else "FAILED"
        if not succeeded:
            failed_at = name

    return results, call_counts, failed_at

def print_report(label, fail_stages):
    results, call_counts, failed_at = run_pipeline(fail_stages)
    print(f"=== {label} ===")
    for name in STAGE_ORDER:
        print(f"  {name:18s} -> {results[name]:8s}  (call_count={call_counts[name]})")
    if failed_at is None:
        print("  ফলাফল: পাইপলাইন সবুজ -- সব ৫টি স্টেজ সফল হয়েছে\n")
    else:
        print(f"  ফলাফল: '{failed_at}' স্টেজে ব্যর্থতার কারণে থেমে গেছে -- পরের স্টেজগুলো কখনো চালানো হয়নি\n")
    return call_counts

# রান ১ -- সব স্টেজ সফল
print_report("রান ১ -- সব স্টেজ পাস", fail_stages=set())

# রান ২ -- 'integration_test' স্টেজে ইচ্ছাকৃতভাবে ব্যর্থতা ইনজেক্ট করা হলো
call_counts_run2 = print_report(
    "রান ২ -- 'integration_test' স্টেজে ইনজেক্টেড ব্যর্থতা",
    fail_stages={"integration_test"},
)

# --- যাচাই: skip করা 'security_scan' ও 'deploy' স্টেজের ফাংশন সত্যিই কখনো কল হয়নি ---
assert call_counts_run2["lint"] == 1, "lint স্টেজ ঠিক একবার কল হওয়ার কথা"
assert call_counts_run2["unit_test"] == 1, "unit_test স্টেজ ঠিক একবার কল হওয়ার কথা"
assert call_counts_run2["integration_test"] == 1, "integration_test নিজেই একবার কল হয়ে ব্যর্থ হওয়ার কথা"
assert call_counts_run2["security_scan"] == 0, "security_scan ভুলবশত কল হয়ে গেছে!"
assert call_counts_run2["deploy"] == 0, "deploy ভুলবশত কল হয়ে গেছে!"

print(
    f"যাচাই সফল: রান ২-এ security_scan এর call_count={call_counts_run2['security_scan']}, "
    f"deploy এর call_count={call_counts_run2['deploy']} -- অর্থাৎ integration_test ব্যর্থ হওয়ার পর "
    f"এই দুটো স্টেজের ফাংশন সত্যিই একবারও চালানো হয়নি, শুধু 'SKIPPED' হিসেবে দেখানো হয়েছে।"
)

    
রান ১-এ fail_stages খালি সেট, তাই পাঁচটি স্টেজেরই call_count হয় 1 এবং failed_at থাকে None। রান ২-এ integration_test কল হওয়ার সময় (তার নিজের call_count ১-এ যায়) False রিটার্ন করে, কারণ "integration_test" in fail_stages — সাথে সাথে failed_at = "integration_test" সেট হয়ে যায়। লুপের পরের ধাপগুলোতে (security_scan, deploy) if failed_at is not None শর্ত সত্যি হওয়ায় সেই স্টেজের ফাংশন একবারও কল করা হয় না — তাদের call_count শূন্যই থেকে যায়, যা কোড সেলের পাঁচটি assertই নিশ্চিত করেছে।

৩ · কেন টেস্ট-টাইপ-বিশেষায়িত স্টেজ বিভাজন গুরুত্বপূর্ণ

../full-stack-web-frameworks/ ও ../mobile-app-development/-এর CI/CD লেসনগুলোতে "test" একটি একক স্টেজ হিসেবে দেখানো হয়েছে, কারণ সেই কোর্সগুলোর ফোকাস ডিপ্লয়মেন্ট প্রক্রিয়া নিজেই। এই কোর্স যেহেতু টেস্টিং-এর ফ্ল্যাগশিপ কোর্স, এখানে সেই একক "test" স্টেজকে ভেঙে unit_test (M3), integration_test (M5), আর security_scan (M9)-এ আলাদা করে দেখানো হয়েছে — যাতে স্পষ্ট বোঝা যায় দ্রুততম ও সস্তা টেস্ট টাইপ (ইউনিট) আগে চলে, ধীর ও ব্যয়বহুল টেস্ট টাইপ (ইন্টিগ্রেশন, সিকিউরিটি স্ক্যান) পরে চলে — একই "সস্তা আগে, ব্যয়বহুল পরে" নীতি যা M7/L27-এর টেস্ট অটোমেশন পিরামিডেও দেখা গেছে।

মূল কথা · Key takeaway

একটি কন্টিনিউয়াস টেস্টিং পাইপলাইন এই কোর্সে শেখা প্রতিটি টেস্ট টাইপকে একটি নির্দিষ্ট, ক্রমিক স্টেজে বসিয়ে দেয় — lint (স্ট্যাটিক চেক) → unit_test (M3) → integration_test (M5) → security_scan (M9) → deploy। প্রথম ব্যর্থতাতেই বাকি সব স্টেজ (এবং সেই স্টেজের সম্ভাব্য ঝুঁকিপূর্ণ কাজ, যেমন প্রোডাকশনে ডিপ্লয় করা) সম্পূর্ণ এড়িয়ে যাওয়া হয় — এবং এই early-stop আচরণ নিছক প্রিন্ট করা বার্তা নয়, বরং উপরের কোডে দেখানো real call-count দিয়ে প্রমাণিত।

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

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

প্র ০১ কেন unit_test স্টেজ integration_test-এর আগে রাখা হয়েছে, উল্টো ক্রমে নয়?

ইউনিট টেস্ট (M3) ছোট, বিচ্ছিন্ন এবং দ্রুত চলে — কয়েক সেকেন্ডে হাজার হাজার ইউনিট টেস্ট চালানো সম্ভব। ইন্টিগ্রেশন টেস্ট (M5) একাধিক মডিউল/সার্ভিস একসাথে চালায়, তাই সাধারণত ধীর ও বেশি রিসোর্স খরচ করে। "সস্তা ও দ্রুত আগে, ব্যয়বহুল ও ধীর পরে" নীতি অনুযায়ী (M7/L27-এর টেস্ট অটোমেশন পিরামিডের একই যুক্তি), ইউনিট টেস্ট আগে চালালে বেশিরভাগ সাধারণ বাগ দ্রুত ও কম খরচে ধরা পড়ে, ইন্টিগ্রেশন টেস্টের ধীর রান শুধু তখনই ঘটে যখন ইউনিট টেস্ট ইতিমধ্যে পাস করেছে।

প্র ০২ যদি fail_stages = {"lint", "security_scan"} দিয়ে পাইপলাইন চালানো হতো, তাহলে security_scan-এর call_count কত হতো, আর কেন?

0 হতো — কারণ lint নিজেই প্রথম স্টেজ এবং "lint" in fail_stages সত্যি হওয়ায় প্রথম কল-এই ব্যর্থ হবে (call_count["lint"] = 1) এবং failed_at = "lint" সেট হয়ে যাবে। এরপর unit_test, integration_test, security_scan, deploy — সবগুলো "SKIPPED" হবে এবং কখনো কল হবে না। security_scan-কে ইচ্ছাকৃতভাবে ব্যর্থ করার পরিকল্পনা থাকলেও, সেই কোড কখনো চলারই সুযোগ পায়নি — পাইপলাইন প্রথম ব্যর্থতাতেই থেমে যায়, একসাথে একাধিক ব্যর্থতা "খুঁজে বের করার" চেষ্টা করে না।

প্র ০৩ security_scan স্টেজটি deploy-এর ঠিক আগে বসানো হয়েছে, একেবারে শুরুতে নয় কেন?

M12/L51-এর শিফট-লেফট নীতি অনুযায়ী সিকিউরিটি-সম্পর্কিত অনেক চেক (থ্রেট মডেলিং, কোড রিভিউ) আসলে আরও আগে, ডিজাইন/কোডিং ধাপেই হওয়া উচিত — কিন্তু security_scan স্টেজটি একটি নির্দিষ্ট ধরনের স্বয়ংক্রিয় স্ক্যান (M9-এর SAST-স্টাইল প্যাটার্ন স্ক্যানের মতো, M9/L37) যা সম্পূর্ণ, বিল্ড-হওয়া কোডবেসের বিরুদ্ধে চালানো সবচেয়ে কার্যকর। এটি deploy-এর ঠিক আগে রাখা হয়েছে কারণ এটি সর্বশেষ স্বয়ংক্রিয় "গেট" — প্রোডাকশনে যাওয়ার আগে একটি নিরাপত্তা-নির্দিষ্ট নিশ্চয়তা, লিন্ট/ইউনিট/ইন্টিগ্রেশন টেস্ট থেকে ভিন্ন উদ্দেশ্যে।

অনুশীলন

  1. চিন্তা করুন: এই কোর্সে শেখা পারফরম্যান্স টেস্টিং (M8) এই পাঁচ-স্টেজ পাইপলাইনে কোথায় যোগ করবেন বলে মনে করেন, এবং কেন?

    সাধারণত security_scan-এর কাছাকাছি, deploy-এর আগে একটি আলাদা performance_test স্টেজ হিসেবে যুক্তিসঙ্গত — কারণ M8-এ শেখা লোড/স্ট্রেস টেস্টিং (M8/L32) সম্পূর্ণ, ইন্টিগ্রেশন-টেস্ট-পাস-করা বিল্ডের বিরুদ্ধে চালানো সবচেয়ে অর্থপূর্ণ, এবং এটি সাধারণত ইউনিট/ইন্টিগ্রেশন টেস্টের চেয়ে ধীর ও রিসোর্স-ভারী — তাই "সস্তা আগে, ব্যয়বহুল পরে" নীতি অনুযায়ী পাইপলাইনের শেষের দিকে বসে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় রান যোগ করুন — print_report("রান ৩ -- 'lint' স্টেজে ব্যর্থতা", fail_stages={"lint"}) — এবং Run চেপে দেখুন বাকি চারটি স্টেজের call_count কত হয়।

    বাকি চারটি স্টেজ (unit_test, integration_test, security_scan, deploy) — প্রতিটির call_count হবে 0, কারণ lint পাইপলাইনের প্রথম স্টেজ এবং এটি ব্যর্থ হওয়ার সাথে সাথে failed_at = "lint" সেট হয়ে যায় — এরপর লুপের প্রতিটি পরবর্তী ধাপেই if failed_at is not None শর্ত সত্যি থাকায় কোনো স্টেজ ফাংশনই আর কল হয় না। এটি দেখায় প্রথম স্টেজে ব্যর্থতা হলে পুরো পাইপলাইনই কার্যত "না-চালানো" থেকে যায়।

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

আগের পাঠ
বিহেভিয়ার-ড্রিভেন ডেভেলপমেন্ট (BDD)