CI/CD পাইপলাইনে কন্টিনিউয়াস টেস্টিং
এই পাঠে যা শিখবেন
- একটি টেস্টিং-কেন্দ্রিক 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" স্টেজকে ভেঙে দেখানো হয়েছে ঠিক কোন টেস্ট টাইপ কোথায় বসে।
unit_test, integration_test, security_scan-এ ভেঙে দেখানো হয়েছে, যাতে বোঝা যায় এই কোর্সের প্রতিটি টেস্ট টাইপ পাইপলাইনে ঠিক কোথায় বসে।কোড স্টাইল ও সাধারণ সিনট্যাক্স ভুল দ্রুত ধরে — সবচেয়ে সস্তা চেক, তাই প্রথমে চলে (M12/L51-এর শিফট-লেফট নীতির সাথে সামঞ্জস্যপূর্ণ)।
M3-এ শেখা
unittest-ভিত্তিক ইউনিট টেস্ট — ছোট, বিচ্ছিন্ন ফাংশন/ক্লাসের আচরণ যাচাই করে, দ্রুত চলে।M5-এ শেখা ইন্টিগ্রেশন টেস্ট — একাধিক মডিউল/সার্ভিস একসাথে সঠিকভাবে কাজ করছে কি না যাচাই করে, ইউনিট টেস্টের চেয়ে ধীর।
M9-এ শেখা সিকিউরিটি টেস্টিং (যেমন SAST-স্টাইল স্ক্যান, M9/L37) — কোড ডিপ্লয় হওয়ার আগে সাধারণ নিরাপত্তা ঝুঁকি খোঁজে।
২ · একটি সত্যিকারের পাইপলাইন রানার — early-stop ও কল-কাউন্ট যাচাই
নিচের কোড সেলে প্রতিটি স্টেজ একটি real ফাংশন — কল হলে নিজের call_counts কাউন্টার বাড়ায়, এবং
একটি ইনজেক্ট করা fail_stages সেটে নিজের নাম থাকলে False রিটার্ন করে।
run_pipeline() স্টেজগুলো ক্রমানুসারে চালায় — একটি স্টেজ ব্যর্থ হলে পরের সব স্টেজকে
"SKIPPED" চিহ্নিত করে দেয়, কিন্তু তাদের ফাংশন কখনো কল করে না। রান ১-এ সব স্টেজ সফল হবে;
রান ২-এ integration_test-এ ইচ্ছাকৃতভাবে ব্যর্থতা ইনজেক্ট করা হয়েছে — তারপর
security_scan ও deploy-এর call_count সত্যিই 0
কি না তা assert দিয়ে যাচাই করা হয়েছে।
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-এর টেস্ট
অটোমেশন পিরামিডেও দেখা গেছে।
একটি কন্টিনিউয়াস টেস্টিং পাইপলাইন এই কোর্সে শেখা প্রতিটি টেস্ট টাইপকে একটি নির্দিষ্ট, ক্রমিক স্টেজে বসিয়ে দেয় — 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-এর ঠিক আগে রাখা হয়েছে কারণ এটি
সর্বশেষ স্বয়ংক্রিয় "গেট" — প্রোডাকশনে যাওয়ার আগে একটি নিরাপত্তা-নির্দিষ্ট নিশ্চয়তা, লিন্ট/ইউনিট/ইন্টিগ্রেশন
টেস্ট থেকে ভিন্ন উদ্দেশ্যে।
অনুশীলন
-
চিন্তা করুন: এই কোর্সে শেখা পারফরম্যান্স টেস্টিং (M8) এই পাঁচ-স্টেজ পাইপলাইনে কোথায়
যোগ করবেন বলে মনে করেন, এবং কেন?
সাধারণত
security_scan-এর কাছাকাছি,deploy-এর আগে একটি আলাদাperformance_testস্টেজ হিসেবে যুক্তিসঙ্গত — কারণ M8-এ শেখা লোড/স্ট্রেস টেস্টিং (M8/L32) সম্পূর্ণ, ইন্টিগ্রেশন-টেস্ট-পাস-করা বিল্ডের বিরুদ্ধে চালানো সবচেয়ে অর্থপূর্ণ, এবং এটি সাধারণত ইউনিট/ইন্টিগ্রেশন টেস্টের চেয়ে ধীর ও রিসোর্স-ভারী — তাই "সস্তা আগে, ব্যয়বহুল পরে" নীতি অনুযায়ী পাইপলাইনের শেষের দিকে বসে। -
পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় রান যোগ করুন —
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-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ: কেস স্টাডি — একটি বাস্তব অ্যাপের জন্য টেস্টিং স্ট্র্যাটেজি পাঠ ৫৫ এই মডিউল পর্যন্ত শেখা সব টেস্ট টাইপ একটি হাইপোথেটিক্যাল ই-কমার্স চেকআউট ফ্লো-তে কীভাবে একসাথে বসে তা দেখুন।
- ফুল-স্ট্যাক প্রজেক্টের জন্য CI/CD পাইপলাইন সহোদর পাঠ একই early-stop-on-failure যন্ত্রণা, ডিপ্লয়মেন্ট-ফোকাসড lint/test/build/deploy স্টেজ ক্রমে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন।