মোবাইলের জন্য UI/অটোমেশন টেস্টিং
এই পাঠে যা শিখবেন
- একটি স্বনির্ভর টেস্ট-রানার (
testডেকোরেটর +run_all()) কীভাবে কাজ করে - একটি নেভিগেশন স্ট্যাক ক্লাসের
push/pop/currentলজিক assert-ভিত্তিক টেস্ট দিয়ে যাচাই করা - কেন শুধু "টেস্ট লেখা" যথেষ্ট নয় — টেস্ট-রানারটি সত্যিই ভুল কোড ধরতে পারে কি না তা নিজেই প্রমাণ করা জরুরি
- একই টেস্ট-লজিক দুটো ভিন্ন বাস্তবায়নের (সঠিক বনাম বাগযুক্ত) বিরুদ্ধে চালিয়ে ফলাফলের পার্থক্য পড়া
১ · স্বনির্ভর টেস্ট-রানার — test ও run_all()
একটি রিয়েল UI/automation টেস্ট আসলে ডিভাইসে ট্যাপ পাঠায় ও স্ক্রিন যাচাই করে — এই Pyodide স্যান্ডবক্সে সেই
ডিভাইস অংশ নেই, কিন্তু টেস্ট করার লজিকটা হুবহু বাস্তব: একটি টেস্ট ফাংশন লেখা হয়, তার ভেতর
assert দিয়ে প্রত্যাশা যাচাই করা হয়, আর একটি রানার সব টেস্ট চালিয়ে কোনটি পাস আর কোনটি ফেইল হলো তা
রিপোর্ট করে। নিচের test(name) ডেকোরেটর প্রতিটি টেস্ট ফাংশনকে একটি তালিকায় জমা করে, আর
run_all() প্রতিটি ফাংশন চালিয়ে AssertionError ধরে ফেলে — অর্থাৎ একটি টেস্ট ব্যর্থ
হলেও পুরো প্রোগ্রাম ক্র্যাশ করে না, শুধু সেই টেস্টটিকে [FAIL] হিসেবে রিপোর্ট করে।
test(name)একটি ডেকোরেটর — যেকোনো ফাংশনকে
(name, fn) জোড়া হিসেবে একটি অভ্যন্তরীণ তালিকায় নিবন্ধন করে।run_all()নিবন্ধিত প্রতিটি টেস্ট চালায়,
AssertionError ধরে ফেলে, এবং পাস/ফেইল সংখ্যা গুনে রিপোর্ট করে।যে লজিক আসলে একটি স্ক্রিন-ট্রানজিশনের পেছনে চলে (এখানে: নেভিগেশন স্ট্যাক) — device ছাড়াই সরাসরি assert দিয়ে যাচাইযোগ্য।
২ · একটি নেভিগেশন স্ট্যাক টেস্ট করা
নিচে একটি ছোট্ট NavStack ক্লাস — যা একটি বাস্তব মোবাইল অ্যাপের স্ক্রিন-ট্রানজিশনের পেছনের লজিক
(M6-এর স্টাইলে, এই পাঠে স্বনির্ভরভাবে সংজ্ঞায়িত)। এর একটি সচেতন নীতি আছে: root screen কখনো
pop করা যায় না — বাস্তবেও একটি অ্যাপের হোম স্ক্রিন থেকে আরও পেছনে ব্যাক করার কিছু নেই। চারটি
assert-ভিত্তিক টেস্ট দিয়ে এই লজিক যাচাই করা হচ্ছে।
# ---------- CORE PATTERN: স্বনির্ভর টেস্ট-রানার, এই পাঠের জন্য পুনর্গঠিত ----------
_tests = []
def test(name):
def decorator(fn):
_tests.append((name, fn))
return fn
return decorator
def run_all(test_list=None):
items = test_list if test_list is not None else _tests
passed, failed = 0, 0
for name, fn in items:
try:
fn()
print(f"[PASS] {name}")
passed += 1
except AssertionError as e:
print(f"[FAIL] {name} -- {e}")
failed += 1
print(f"মোট: {len(items)}, পাস: {passed}, ফেইল: {failed}\n")
return passed, failed
# ---------- নেভিগেশন স্ট্যাক -- root screen কখনো pop করা যায় না ----------
class NavStack:
def __init__(self, root):
self.stack = [root]
def push(self, screen):
self.stack.append(screen)
def pop(self):
if len(self.stack) <= 1:
return None # root screen -- pop করা হবে না
return self.stack.pop()
def current(self):
return self.stack[-1]
# ---------- এই লজিকের উপর assert-ভিত্তিক টেস্ট ----------
@test("প্রাথমিক স্ট্যাকে শুধু root screen থাকে")
def _():
nav = NavStack("Home")
assert nav.current() == "Home"
assert nav.stack == ["Home"]
@test("push নতুন screen যোগ করে current() আপডেট করে")
def _():
nav = NavStack("Home")
nav.push("Profile")
assert nav.current() == "Profile"
assert nav.stack == ["Home", "Profile"]
@test("pop সঠিক screen সরিয়ে আগের screen-এ current() ফিরিয়ে আনে")
def _():
nav = NavStack("Home")
nav.push("Profile")
nav.push("Settings")
popped = nav.pop()
assert popped == "Settings"
assert nav.current() == "Profile"
@test("root screen থেকে pop করলে None রিটার্ন হয়, স্ট্যাক অপরিবর্তিত থাকে")
def _():
nav = NavStack("Home")
result = nav.pop()
assert result is None
assert nav.stack == ["Home"]
print("== NavStack-এর বিরুদ্ধে সম্পূর্ণ টেস্ট স্যুট ==")
run_all()
[PASS] হবে (মোট: ৪, পাস: ৪, ফেইল: ০)। কিন্তু এখানে একটি স্বাভাবিক প্রশ্ন ওঠে —
এই রানারটি কি সত্যিই ভুল কোড ধরতে সক্ষম, নাকি এটি শুধু কখনো ব্যর্থ না হওয়া টেস্ট দিয়ে চালানো হয়েছে বলে
সবসময় পাস দেখাচ্ছে? পরের সেকশনে এই প্রশ্নের উত্তর সরাসরি পরীক্ষা করে দেখা যাক।
৩ · ইচ্ছাকৃত বাগ — রানার কি সত্যিই FAIL ধরতে পারে?
নিচে NavStack-এর একটি ইচ্ছাকৃতভাবে বাগযুক্ত কপি BuggyNavStack — এর
pop() কোনো গার্ড ছাড়াই সরাসরি self.stack.pop() কল করে, root screen-কেও সরিয়ে
দেয়। একই টেস্ট-লজিক (make_root_guard_test ফাংশন হিসেবে) দুইবার চালানো হচ্ছে — একবার
BuggyNavStack-এর বিরুদ্ধে (ফেইল হওয়া উচিত), একবার সঠিক NavStack-এর বিরুদ্ধে
(পাস হওয়া উচিত)।
# ---------- ইচ্ছাকৃত বাগ: root screen guard ছাড়াই সরাসরি pop() করে ----------
class BuggyNavStack(NavStack):
def pop(self):
return self.stack.pop() # গার্ড নেই -- root screen-ও সরিয়ে দেয়
def make_root_guard_test(nav):
"""একই টেস্ট-লজিক, শুধু ভিন্ন nav instance দিয়ে চালানো যায় (BuggyNavStack বা NavStack)।"""
def t():
result = nav.pop()
assert result is None, (
f"root screen থেকে pop করা উচিত ছিল না, কিন্তু pop() রিটার্ন করলো: {result!r}"
)
assert nav.stack == ["Home"], (
f"স্ট্যাক অপরিবর্তিত থাকা উচিত ছিল, পাওয়া গেছে: {nav.stack}"
)
return t
bug_demo_tests = [
("BuggyNavStack -- root screen guard (এখানে বাগ আছে)", make_root_guard_test(BuggyNavStack("Home"))),
("NavStack (সঠিক সংস্করণ) -- একই টেস্ট লজিক", make_root_guard_test(NavStack("Home"))),
]
print("== একই টেস্ট লজিক, দুটো ভিন্ন বাস্তবায়নের বিরুদ্ধে ==")
run_all(bug_demo_tests)
BuggyNavStack("Home").pop()-এ কোনো দৈর্ঘ্য-চেক না থাকায় এটি সরাসরি "Home" পপ করে
রিটার্ন করে দেয় (স্ট্যাক খালি হয়ে যায়) — assert result is None এখানে ব্যর্থ হয়
("Home" কখনো None নয়), তাই run_all() এটিকে সত্যিকারের
[FAIL] হিসেবে রিপোর্ট করে, সাথে ঠিক কোন মান পাওয়া গেছে সেই বার্তাসহ। দ্বিতীয় এন্ট্রিতে সঠিক
NavStack-এ len(self.stack) <= 1 গার্ডের কারণে pop()
None রিটার্ন করে এবং স্ট্যাক অপরিবর্তিত থাকে — উভয় assert পাস করে, [PASS] রিপোর্ট
হয় (মোট: ২, পাস: ১, ফেইল: ১)। দুটো টেস্টেই হুবহু একই make_root_guard_test
লজিক ব্যবহার হয়েছে — শুধু পাশ করানো instance আলাদা — যা প্রমাণ করে রানারটি সত্যিকারের আচরণের পার্থক্য ধরতে
পারে, নিছক সবসময় "পাস" বলে না।
একটি UI/automation টেস্ট-রানারের আসল মূল্য তার FAIL সঠিকভাবে ধরতে পারার ক্ষমতায় — শুধু সবুজ রিপোর্ট
দেখানোয় নয়। এই পাঠে একই টেস্ট-লজিক একটি বাগযুক্ত ও একটি সঠিক বাস্তবায়নের বিরুদ্ধে চালিয়ে দেখানো হয়েছে
রানারটি সত্যিই পার্থক্য ধরতে পারে। এই একই স্বনির্ভর test/run_all() প্যাটার্ন
এই কোর্সের বাকি M12 পাঠগুলোতেও পুনরায় ব্যবহৃত হবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
যদি run_all()-এ try/except AssertionError ব্লকটি না থাকত, তাহলে
একাধিক টেস্ট চালানোর সময় কী ঘটত?
প্রথম যে টেস্টটি assert ব্যর্থ হতো, সেখানেই AssertionError পুরো প্রোগ্রামটি
ক্র্যাশ করিয়ে দিত এবং লুপের বাকি টেস্টগুলো কখনোই চলত না — অর্থাৎ একটি ব্যর্থ টেস্ট বাকি সব টেস্টের
ফলাফল লুকিয়ে ফেলত। try/except ব্লকটিই নিশ্চিত করে প্রতিটি টেস্ট স্বাধীনভাবে চলে এবং
একটির ব্যর্থতা অন্যগুলোর রিপোর্টিংকে প্রভাবিত করে না।
প্র ০২
দ্বিতীয় কোড সেলে run_all(bug_demo_tests) কল শেষে ঠিক কী প্রিন্ট হবে — "মোট", "পাস", "ফেইল" সংখ্যা কত?
"মোট: ২, পাস: ১, ফেইল: ১" — কারণ bug_demo_tests তালিকায় দুটো এন্ট্রি আছে, প্রথমটি
(BuggyNavStack) ব্যর্থ হয়, দ্বিতীয়টি (সঠিক NavStack) সফল হয়। মনে রাখা
গুরুত্বপূর্ণ — এই কলে _tests তালিকার আসল চারটি টেস্ট ব্যবহৃত হয়নি, কারণ
run_all(bug_demo_tests)-এ স্পষ্টভাবে একটি ভিন্ন test_list পাস করা হয়েছে।
প্র ০৩
make_root_guard_test(nav) ফাংশনটি কেন একটি nav instance প্যারামিটার হিসেবে
নেয়, সরাসরি ভেতরে NavStack("Home") তৈরি না করে?
কারণ তাহলে একই টেস্ট-লজিক দুটো ভিন্ন ক্লাসের (NavStack ও BuggyNavStack)
বিরুদ্ধে পুনরায় ব্যবহার করা সম্ভব হয় — কোড ডুপ্লিকেট না করেই। এটিই প্রমাণ করে যে ফেইল/পাসের পার্থক্যটা
টেস্ট-লজিকে নয়, বরং বাস্তবায়নে — ঠিক যেভাবে বাস্তব CI-তে একই টেস্ট স্যুট একাধিক
বিল্ড/সংস্করণের বিরুদ্ধে চালানো হয়।
অনুশীলন
-
চিন্তা করুন: উপরের প্রথম কোড সেলের চারটি টেস্টের মধ্যে কোনটি একটি "একক ধাপ" (single-step)
যাচাই করে, আর কোনটি একাধিক ধাপের একটি ক্রম (push, push, pop) যাচাই করে? এই পার্থক্যটা কি ইউনিট বনাম
ইন্টিগ্রেশন টেস্টের ধারণার সাথে সাদৃশ্যপূর্ণ মনে হয়?
প্রথম, দ্বিতীয় ও চতুর্থ টেস্ট প্রতিটি একটিমাত্র অপারেশন (init/push/pop) যাচাই করে — এগুলো unit-স্তরের কাছাকাছি। তৃতীয় টেস্টটি একাধিক ধাপের ক্রম (দুটো push তারপর একটি pop) যাচাই করে — এটি integration-স্তরের কাছাকাছি, কারণ এটি একাধিক অপারেশন একসাথে সঠিকভাবে কাজ করছে কি না দেখে। L50-এর পিরামিড ধারণার সাথে এটি মিলে যায় — বেশিরভাগ টেস্টই ছোট, বিচ্ছিন্ন ধাপ যাচাই করা উচিত।
-
পরীক্ষা করুন: উপরের প্রথম কোড সেলে একটি নতুন টেস্ট যোগ করুন যা যাচাই করে —
pushদুইবার করার পরpopদুইবার করলে স্ট্যাক আবার শুধু["Home"]-এ ফিরে আসে এবং তৃতীয়বারpopকরলেNoneরিটার্ন হয়।এভাবে লেখা যায়:
@test("দুইবার push তারপর দুইবার pop -- root-এ ফিরে আসে")
def _(): nav = NavStack("Home"); nav.push("A"); nav.push("B"); nav.pop(); nav.pop(); assert nav.stack == ["Home"]; assert nav.pop() is None
এই টেস্টটিrun_all()-এর ডিফল্ট তালিকায় স্বয়ংক্রিয়ভাবে যুক্ত হয়ে যাবে (কারণ@test(...)ডেকোরেটর সেটিকে_tests-এ নিবন্ধন করে দেয়), এবং কোডটি আবার চালালে মোট টেস্ট সংখ্যা ৪ থেকে ৫-এ বেড়ে যাবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- L50 · মোবাইল টেস্টিং পিরামিড আগের পাঠ unit/integration/UI স্তরের অনুপাত ও মোবাইলে UI টেস্টের বাড়তি খরচ এই পাঠেই দেখানো হয়েছে।
- সব 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 ও Mobile App Development — সব এক জায়গায়।