পাইটেস্ট ইনস্টল ও প্রথম টেস্ট
হাতে চালিয়ে দেখা কেন যথেষ্ট নয়, খালি assert কেন প্রায় কিছুই বলে না, আর pytest ইনস্টল করে প্রথম টেস্ট ফাইল লেখা ও চালানো। ডট, সারাংশ লাইন, -q, -v আর exit code পড়াসহ।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
এই যে একটি ছোট ফাংশন, আর এ রকম ফাংশন আমরা বেশিরভাগই যেভাবে যাচাই করি — চালিয়ে দেখি কী বের হয়:
def discounted(price, percent):
return price - price * percent / 100
print(discounted(200, 10))
print(discounted(80, 25))
print(discounted(99, 0))180.0
60.0
99.0তিনটি সংখ্যা পড়লেন, মনে মনে হিসাব কষলেন, আর ঠিক করলেন যে ফলগুলো ঠিক আছে। এটাও একটা টেস্ট। তবে হাতে-করা টেস্ট, আর এর দুটি সমস্যা আছে।
প্রথম সমস্যা, এটা বড় হতে পারে না। তিনটি সংখ্যা চোখে দেখে মেলানো সহজ। কিন্তু ত্রিশটি ফাংশন, প্রতিটির পাঁচটি করে ঘটনা, প্রতিবার কোড বদলানোর পর আবার মেলানো — সেটা সহজ নয়, আর কেউ তা করেও না।
দ্বিতীয় সমস্যাটা আরও খারাপ। এক মাস পরে কেউ ফাংশনটিকে একটু গুছিয়ে লিখল:
def discounted(price, percent):
return price * (1 - percent // 100)$ python -c "from prices import discounted; print(discounted(200, 10))"
200দেখতে যুক্তিসংগত, আর কোনো ত্রুটি ছাড়াই চলে। কিন্তু // হলো পূর্ণসংখ্যার ভাগ, তাই 10 // 100 হয় 0, আর সব ছাড় নিঃশব্দে উধাও হয়ে গেছে। যে কোড আগে কাজ করত কিন্তু এখন আর করে না, তাকে বলে রিগ্রেশন (regression), আর হাতে-করা যাচাই ঠিক এটাই ধরতে পারে না — কারণ পুরোনো সংখ্যাগুলো আবার মিলিয়ে দেখতে কেউ ফিরে যায় না।
সমাধান হলো প্রত্যাশিত উত্তরগুলো কোডে লিখে রাখা, যাতে একটি যন্ত্র প্রতিবার সেগুলো যাচাই করতে পারে। এর জন্য পাইথনে আগে থেকেই একটি স্টেটমেন্ট আছে, assert। যাচাইগুলো prices.py-এর পাশে check_prices.py নামের একটি ফাইলে রাখুন:
from prices import discounted
assert discounted(200, 10) == 180.0
assert discounted(80, 25) == 60.0
assert discounted(99, 0) == 99
print("all good")আসল ফাংশনটি দিয়ে চালালে:
$ python check_prices.py
all goodআর "গোছানো" ফাংশনটি দিয়ে:
$ python check_prices.py
Traceback (most recent call last):
File "/home/you/shop/check_prices.py", line 3, in <module>
assert discounted(200, 10) == 180.0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorরিগ্রেশনটি ধরা পড়েছে, এটা অগ্রগতি। কিন্তু দেখুন আপনাকে কী জানানো হলো: AssertionError, ব্যস, আর কিছু না। discounted(200, 10) আসলে কী ফেরত দিল, তা-ও নয়। আর স্ক্রিপ্টটি প্রথম ব্যর্থতাতেই থেমে গেছে, তাই বাকি দুটি যাচাই পাস করে কি না, আপনি জানেন না।
এই অধ্যায়ে আমরা সেই টুলটি ইনস্টল করব যেটা দুটি সমস্যাই সমাধান করে: pytest।
এই অধ্যায় শেষে আপনি পারবেন
- বোঝাতে, কেন স্বয়ংক্রিয় টেস্ট সেই রিগ্রেশনগুলো ধরে যা হাতে-করা যাচাই ধরতে পারে না
uvবাpipদিয়ে pytest ইনস্টল করতে, আরpytest --versionদিয়ে নিশ্চিত হতে- এমন একটি টেস্ট ফাইল লিখতে যা pytest নিজে থেকেই খুঁজে পায়, আর সেটি চালাতে
- বিন্দুগুলো আর সারাংশ লাইনটি পড়তে, আর
-qও-v-এর মধ্যে বদলাতে - বলতে, এক্সিট কোড
0আর1মানে কী, আর সেটা কেন গুরুত্বপূর্ণ
আগে যা জানা লাগবে: টেস্ট লেখার আগে। এ ছাড়া পাইথন 3 থাকতে হবে, আর একটি ফাংশন লিখতে জানতে হবে।
টেস্ট লেখার আগে
আগের অধ্যায় ছিল কী টেস্ট করবেন তা ঠিক করা নিয়ে। এই অধ্যায় সেই বাস্তব জিনিসগুলো নিয়ে, যেগুলো টেস্ট কোডের প্রথম লাইন চলার আগেই জায়গামতো থাকতে হবে। জিনিস চারটি, আর প্রতিটি না থাকলে নিজস্ব একটা বিভ্রান্তিকর ত্রুটি দেখা দেয় — তাই ক্রম ধরে একে একে যাচাই করে নেওয়াই লাভজনক।
১. একটি প্রজেক্ট ফোল্ডার, একটি এনভায়রনমেন্ট। এই অধ্যায়ের সবকিছু একটিমাত্র ফোল্ডারে থাকে:
shop/
├── .venv/ the project's own Python environment
├── pyproject.toml created by uv; lists pytest as a dev dependency
├── prices.py the code under test
└── test_prices.py the testsআলাদা নিজস্ব এনভায়রনমেন্ট কেন? কারণ pytest-কে ঠিক সেই পাইথনেই ইনস্টল করতে হবে যেটা আপনার কোড ইমপোর্ট করবে। pytest যদি এক পাইথনে থাকে আর প্রজেক্টের প্যাকেজগুলো আরেকটিতে, তাহলে এমন ত্রুটি পাবেন যা দেখে মনে হয় কোড খুঁজে পাওয়া যাচ্ছে না, অথচ আসলে অনুপস্থিত হলো টুলটি। প্রতিটি প্রজেক্টের জন্য একটি .venv রাখলে এই অনুমানের খেলা থাকে না।
২. সেই এনভায়রনমেন্টে pytest ইনস্টল করা — পরের অংশে এটাই করা হবে।
৩. কোডটি ইমপোর্ট করা যায়। টেস্ট ফাইল সাধারণ পাইথন ফাইলই: তার প্রথম লাইন হবে from prices import discounted। তাই কোনো টেস্ট লেখার আগে প্রজেক্ট ফোল্ডার থেকে ঠিক এই ইমপোর্টটি কাজ করে কি না দেখে নিন:
$ python -c "from prices import discounted; print(discounted(200, 10))"
180.0এই লাইন ব্যর্থ হলে pytest-ও একইভাবে ব্যর্থ হবে — শুধু একটা লম্বা রিপোর্টে মোড়ানো অবস্থায়। আলাদা করে যাচাই করতে পাঁচ সেকেন্ড লাগে, আর এতে "আমার কোড খুঁজে পাওয়া যাচ্ছে না" আর "আমার কোড ভুল" — এই দুটো আলাদা হয়ে যায়। এর আরেকটা মানে, মডিউলের নাম অবশ্যই বৈধ পাইথন নাম হতে হবে: prices.py ইমপোর্ট করা যায়, my-prices.py আর 2prices.py যায় না।
৪. প্রতিশ্রুতিটি, ঘটনা হিসেবে লিখে রাখা। টেস্ট টাইপ করার আগে লিখে ফেলুন discounted কী প্রতিশ্রুতি দেয়, প্রতিটি ঘটনার জন্য এক সারি:
| ঘটনা | ইনপুট | প্রত্যাশিত | | --- | --- | --- | | সাধারণ ছাড় | discounted(200, 10) | 180.0 | | দ্বিতীয় আরেকটি, ভিন্ন ছাড় | discounted(80, 25) | 60.0 | | প্রান্তিক ঘটনা: কোনো ছাড়ই নেই | discounted(99, 0) | 99 |
আর ঠিক ততটাই সচেতনভাবে ঠিক করুন, তালিকায় কী নেই। ঋণাত্মক শতাংশ, বা ১০০-র বেশি শতাংশ: এগুলো নিয়ে ফাংশনটি এখনো কোনো প্রতিশ্রুতি দেয় না, আর টেস্ট লিখলে সে আজ ঘটনাচক্রে যা করে সেটাকেই শুধু জমিয়ে স্থির করে দেবে। (ভুল ইনপুট ত্রুটি দিয়ে ফিরিয়ে দেওয়া নিজেই একটা আলাদা প্রতিশ্রুতি — চতুর্থ অধ্যায়।) পাইথনের নিজের গাণিতিক হিসাবও তালিকায় নেই: আপনি টেস্ট করছেন আপনার সূত্র, * গুণ করে কি না তা নয়।
একটির বদলে দুটি সাধারণ ঘটনা কেন? তৃতীয় সারিটি অধ্যায়ের শুরুর সেই "গোছানো" ফাংশনের সাথে মিলিয়ে দেখুন: 99 * (1 - 0 // 100) তখনো 99। শূন্য শতাংশের ঘটনাটি বাগ থাকা অবস্থাতেও পাস করে। শুধু প্রান্তিক ঘটনা দিয়ে বানানো পরিকল্পনা ঠিক সেই ভুলটিই ধরতে না পারে যেটা আসলে গুরুত্বপূর্ণ, আর শুধু একটি সাধারণ ঘটনার পরিকল্পনা প্রান্তগুলো ধরতে পারে না। প্রতিটি সারি সেখানে আছে এমন কিছু ধরার জন্য, যা বাকিগুলো ধরত না।
অধ্যায়ের বাকি অংশ এই টেবিলটিকে একটি টেস্ট ফাইলে রূপ দেয়।
pytest ইনস্টল করা
pytest পাইথনের অংশ নয়; এটা একটা প্যাকেজ, যা আপনি নিজের প্রজেক্টে ইনস্টল করেন। এটা কোড তৈরি করার সময়ের টুল, প্রোগ্রাম চলার সময় এর দরকার হয় না — তাই এটা ঢোকে একটি ডেভেলপমেন্ট ডিপেনডেন্সি হিসেবে।
আপনার প্রজেক্ট যদি uv ব্যবহার করে, তাহলে প্রজেক্ট ফোল্ডারে এটা চালান:
$ uv add --dev pytest
Resolved 7 packages in 59ms
Installed 5 packages in 18ms
+ iniconfig==2.3.1
+ packaging==26.3
+ pluggy==1.6.0
+ pygments==2.21.0
+ pytest==9.1.1uv এটাকে pyproject.toml-এ একটি dev গ্রুপের নিচে লিখে রাখে, ফলে পরে যে-ই প্রজেক্টটি সেট আপ করুক, একই টুল পাবে:
[dependency-groups]
dev = [
"pytest>=9.1.1",
]আর যদি ভার্চুয়াল এনভায়রনমেন্টের ভেতরে সাধারণ pip ব্যবহার করেন, তাহলে এনভায়রনমেন্টটি চালু (activate) করে এটা চালান:
$ python -m pip install pytestpip ডাউনলোডের অগ্রগতি দেখায়, আর শেষ করে এভাবে:
Successfully installed iniconfig-2.3.1 packaging-26.3 pluggy-1.6.0 pygments-2.21.0 pytest-9.1.1শুধু pip না লিখে python -m pip লিখলে নিশ্চিত হয় যে প্যাকেজটি ঠিক সেই পাইথনেই বসছে যেটা আপনি চালাবেন — "ইনস্টল তো করলাম, কিন্তু পাচ্ছি না" সমস্যার এটা একটা সাধারণ কারণ।
এবার দেখে নিন কাজ হয়েছে কি না:
$ pytest --version
pytest 9.1.1এগোনোর আগে একটা কথা। uv-এর ক্ষেত্রে কমান্ডের আগে uv run লিখলে সেটা প্রজেক্টের এনভায়রনমেন্টের ভেতরে চলে — যেমন uv run pytest --version। ভার্চুয়াল এনভায়রনমেন্ট চালু থাকলে শুধু pytest-ই যথেষ্ট। এই কোর্সের বাকি অংশে শুধু pytest লেখা থাকবে; আপনি যদি uv দিয়ে কাজ করেন, সামনে uv run জুড়ে নিন।
আপনার প্রথম টেস্ট ফাইল
নাম রাখার দুটি নিয়ম, আর pytest আপনার কাছে এর বেশি কিছু চায় না:
- ফাইলের নাম শুরু হবে
test_দিয়ে (অথবা শেষ হবে_test.pyদিয়ে) - যে ফাংশনগুলো চালাতে চান, সেগুলোর নাম শুরু হবে
testদিয়ে
আসল, সঠিক prices.py ফিরিয়ে আনুন, আর তার পাশে test_prices.py বানান:
from prices import discounted
def test_ten_percent_off():
assert discounted(200, 10) == 180.0
def test_quarter_off():
assert discounted(80, 25) == 60.0
def test_no_discount():
assert discounted(99, 0) == 99check_prices.py-এর প্রতিটি যাচাই এখন নিজস্ব একটি ছোট ফাংশন, আর প্রতিটির নামই বলে দেয় কী যাচাই হচ্ছে। কোনো print নেই, টেস্টের কোনো তালিকা নথিভুক্ত করতে হয় না, কোনো if __name__ == "__main__" নেই। এবার প্রজেক্ট ফোল্ডার থেকে pytest চালান:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 3 items
test_prices.py ... [100%]
============================== 3 passed in 0.01s ===============================pytest ফোল্ডারটা ঘেঁটে দেখেছে, নামের কারণে test_prices.py খুঁজে পেয়েছে, নামের কারণেই তিনটি ফাংশন খুঁজে পেয়েছে, প্রতিটি চালিয়েছে, আর ফল জানিয়েছে।
আউটপুট পড়া
এক লাইন এক লাইন করে দেখা যাক।
platform ...— কোন পাইথন আর কোন pytest চলেছে। দুটি কম্পিউটারে ফল আলাদা হলে এটায় একবার চোখ বুলিয়ে নেওয়া দরকার।rootdir: /home/you/shop— যে ফোল্ডারটিকে pytest প্রজেক্টের শীর্ষ হিসেবে ধরে।collected 3 items— কতগুলো টেস্ট সে খুঁজে পেয়েছে। টেস্ট খুঁজে বের করাকে বলে কালেকশন (collection)। এই সংখ্যা যদি প্রত্যাশার চেয়ে কম হয়, তাহলে কোথাও একটা নাম ভুল আছে।test_prices.py ...— প্রতিটি টেস্টের জন্য একটি করে চিহ্ন, যে ক্রমে চলেছে সেই ক্রমে।.মানে পাস করেছে।[100%]— পুরো রানের অগ্রগতি।3 passed in 0.01s— সারাংশ লাইন। সবার আগে এই লাইনটিই পড়বেন।
এবার "গোছানো" discounted ফিরিয়ে এনে আবার চালান। এবার -q দিয়ে, যার সাথে একটু পরেই ভালোভাবে পরিচয় হবে:
$ pytest -q
FF. [100%]
=================================== FAILURES ===================================
_____________________________ test_ten_percent_off _____________________________
def test_ten_percent_off():
> assert discounted(200, 10) == 180.0
E assert 200 == 180.0
E + where 200 = discounted(200, 10)
test_prices.py:5: AssertionError
_______________________________ test_quarter_off _______________________________
def test_quarter_off():
> assert discounted(80, 25) == 60.0
E assert 80 == 60.0
E + where 80 = discounted(80, 25)
test_prices.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_prices.py::test_ten_percent_off - assert 200 == 180.0
FAILED test_prices.py::test_quarter_off - assert 80 == 60.0
2 failed, 1 passed in 0.01sঅগ্রগতির লাইনটি এখন FF.: প্রতিটি ব্যর্থতার জন্য একটি F। আর আগের সেই শুধু AssertionError-এর সাথে এটা তুলনা করুন:
- প্রতিটি টেস্ট চলেছে — একটির ব্যর্থতা বাকিগুলো থামায়নি, তাই দেখতে পাচ্ছেন যে শূন্য শতাংশ এখনো ঠিক কাজ করে
- প্রতিটি ব্যর্থতা টেস্টটির নাম আর লাইন জানায়
assert 200 == 180.0দেখায় ফাংশনটি আসলে কোন মান ফেরত দিয়েছে
এই রিপোর্ট পুরোটা পড়তে শেখাই পরের অধ্যায়ের পুরো বিষয়। আপাতত শুধু খেয়াল করুন যে সারাংশ লাইনটি বদলে 2 failed, 1 passed হয়েছে — আর যে assert আপনি আগে থেকেই জানতেন, সেটা এখন নিজেই নিজের ব্যাখ্যা দিচ্ছে।
-q আর -v: কম আর বেশি
ডিফল্ট আউটপুট মাঝামাঝি একটা অবস্থা। দুটি ফ্ল্যাগ একে দুদিকে সরায়।
-q (quiet, চুপচাপ) শিরোনামের অংশটা বাদ দেয়:
$ pytest -q
... [100%]
3 passed in 0.01s-v (verbose, বিস্তারিত) প্রতিটি টেস্টকে তার পুরো নামসহ আলাদা লাইন দেয়:
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/shop
collecting ... collected 3 items
test_prices.py::test_ten_percent_off PASSED [ 33%]
test_prices.py::test_quarter_off PASSED [ 66%]
test_prices.py::test_no_discount PASSED [100%]
============================== 3 passed in 0.01s ===============================test_prices.py::test_ten_percent_off হলো টেস্টটির নোড আইডি (node ID): ফাইল, দুটি কোলন, তারপর ফাংশন। পরে একটিমাত্র টেস্ট চালাতে এটা ব্যবহার করবেন। বর্ণনামূলক টেস্টের নামের সুফলও এখানেই — -v তালিকায় test_quarter_off বলে দেয় কী পাস করল; test_2 তা বলত না।
ঘন ঘন টেস্ট চালালে আর শুধু রায়টুকু চাইলে -q ব্যবহার করুন, আর ঠিক কোন কোন টেস্ট চলল তা দেখতে চাইলে -v।
.pytest_cache কী? pytest-এর বানানো একটি ফোল্ডার, যেখানে সে এক রান থেকে আরেক রানে কিছু তথ্য মনে রাখে, যেমন গতবার কোন টেস্টগুলো ব্যর্থ হয়েছিল। এটা মুছে ফেলা নিরাপদ, আর ভার্সন কন্ট্রোলে এর জায়গা নেই।এক্সিট কোড: যন্ত্র কীভাবে ফল পড়ে
আপনি সারাংশ লাইন পড়েন। কিন্তু একটি স্ক্রিপ্ট, এডিটর বা বিল্ড সার্ভার তা পারে না — সে পড়ে এক্সিট কোড, অর্থাৎ যে সংখ্যাটি প্রতিটি প্রোগ্রাম শেষ হওয়ার সময় শেলকে ফেরত দেয়। ইউনিক্স শেলে শেষ এক্সিট কোডটি থাকে $?-তে:
$ pytest -q
... [100%]
3 passed in 0.01s
$ echo $?
0আর ভাঙা ফাংশনটি দিয়ে, যে রানটি 2 failed, 1 passed-এ শেষ হয়েছিল তার পরে:
$ echo $?
10— সবকিছু খুঁজে পাওয়া গেছে, সবকিছু পাস করেছে1— টেস্ট চলেছে, আর অন্তত একটি ব্যর্থ হয়েছে
(PowerShell-এ ভেরিয়েবলটির নাম $LASTEXITCODE।) এই কারণেই কোড মার্জ করার আগে টেস্ট স্বয়ংক্রিয়ভাবে চালানো যায়: শূন্য ছাড়া অন্য কোনো এক্সিট কোড প্রক্রিয়াটি থামিয়ে দেয়। আরও কয়েকটি কোড আছে; তার একটির সাথে নিচে "কিছু ভাঙা অবস্থা ও তার সমাধান" অংশে দেখা হবে।
টেস্ট ফাইল কোথায় থাকে
এই অধ্যায়ে টেস্ট ফাইলটি কোডের ঠিক পাশেই থাকছে:
shop/
├── prices.py
└── test_prices.pyহাতে গোনা কয়েকটি ফাইলের জন্য এটা ঠিক আছে। তবে বেশিরভাগ প্রজেক্ট টেস্টগুলো আলাদা একটি tests/ ফোল্ডারে জড়ো করে রাখে। যেভাবেই রাখুন, pytest নাম দেখেই সেগুলো খুঁজে পায়; একটি সত্যিকারের প্রজেক্ট কীভাবে সাজাতে হয়, আর তাতে কী বদলায়, সেটা তৃতীয় অধ্যায়ের বিষয়।
একটা সম্পূর্ণ উদাহরণ
একটি ফাংশন, যা নম্বরকে লেটার গ্রেডে পরিণত করে, grades.py:
def letter_grade(score):
"""Turn a score from 0 to 100 into a letter."""
if score >= 80:
return "A"
if score >= 60:
return "B"
if score >= 40:
return "C"
return "F"আর তার টেস্টগুলো, test_grades.py:
from grades import letter_grade
def test_top_score_is_an_a():
assert letter_grade(95) == "A"
def test_exactly_eighty_is_still_an_a():
assert letter_grade(80) == "A"
def test_just_below_eighty_is_a_b():
assert letter_grade(79) == "B"
def test_middle_score_is_a_c():
assert letter_grade(50) == "C"
def test_zero_is_an_f():
assert letter_grade(0) == "F"$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/grades
collecting ... collected 5 items
test_grades.py::test_top_score_is_an_a PASSED [ 20%]
test_grades.py::test_exactly_eighty_is_still_an_a PASSED [ 40%]
test_grades.py::test_just_below_eighty_is_a_b PASSED [ 60%]
test_grades.py::test_middle_score_is_a_c PASSED [ 80%]
test_grades.py::test_zero_is_an_f PASSED [100%]
============================== 5 passed in 0.01s ===============================দুটি জিনিস খেয়াল করার মতো।
প্রথমত, 80 আর 79-এর টেস্ট দুটি। এরা একটি সীমানার (boundary) দুই পাশে বসে আছে, আর ভুলগুলো থাকে ঠিক সেখানেই। score >= 80 বদলে score > 80 করুন — খুব সহজেই হয়ে যাওয়া একটা ভুল — আর চুপচাপ রূপটি চালান:
$ pytest -q
.F... [100%]
=================================== FAILURES ===================================
______________________ test_exactly_eighty_is_still_an_a _______________________
def test_exactly_eighty_is_still_an_a():
> assert letter_grade(80) == "A"
E AssertionError: assert 'B' == 'A'
E
E - A
E + B
test_grades.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_grades.py::test_exactly_eighty_is_still_an_a - AssertionError: as...
1 failed, 4 passed in 0.01sদ্বিতীয় টেস্টটি, test_exactly_eighty_is_still_an_a, ব্যর্থ হয়েছে: 80 এখন পাচ্ছে 'B'। শুধু 95-এর একটি টেস্ট থাকলে এটা কখনো ধরা পড়ত না।
দ্বিতীয়ত, নামগুলো। -v তালিকায় প্রতিটি লাইন ফাংশনটির আচরণ সম্পর্কে একটি বাক্যের মতো পড়া যায়। মাসখানেক পরে কোনোটি ব্যর্থ হলে শুধু নাম দেখেই বুঝবেন কোন প্রতিশ্রুতিটি ভেঙেছে।
কিছু ভাঙা অবস্থা ও তার সমাধান
zsh: command not found: pytest (অথবা bash: pytest: command not found) শেল pytest নামের কোনো প্রোগ্রাম খুঁজে পাচ্ছে না। হয় এটা ইনস্টল করা নেই, নয়তো এমন একটি ভার্চুয়াল এনভায়রনমেন্টে ইনস্টল করা আছে যেটা চালু নেই। uv হলে uv run pytest চালান; pip হলে আগে এনভায়রনমেন্টটি চালু করুন। python -m pytest-ও কাজ করে, আর সেটা pytest চালায় python বলতে যে পাইথন বোঝায় সেটা দিয়ে।
/usr/bin/python3: No module named pytest আপনি python -m pytest চালিয়েছেন, আর সেই পাইথনে pytest নেই। শুরুর পাথটি বলে দেয় কোন পাইথন ছিল — এখানে সিস্টেমের পাইথন, আপনার প্রজেক্টেরটি নয়। যে এনভায়রনমেন্ট ব্যবহার করতে চান, pytest সেখানে ইনস্টল করুন।
কালেকশনের সময় ModuleNotFoundError: No module named 'price' টেস্ট ফাইলটি আপনার কোড ইমপোর্ট করতে পারেনি — এখানে একটা টাইপোর কারণে, from prices import ...-এর বদলে লেখা হয়েছে from price import ...:
$ pytest -q
==================================== ERRORS ====================================
_______________________ ERROR collecting test_prices.py ________________________
ImportError while importing test module '/home/you/shop/test_prices.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_prices.py:1: in <module>
from price import discounted
E ModuleNotFoundError: No module named 'price'
=========================== short test summary info ============================
ERROR test_prices.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sখেয়াল করুন শব্দটি ERROR, FAILED নয়: কোনো টেস্টই চলেনি, কারণ ফাইলটি লোডই করা যায়নি, আর এক্সিট কোড 2। শেষ E লাইনটি পড়ুন, তারপর "টেস্ট লেখার আগে" অংশের ইমপোর্ট যাচাইটি — python -c "from prices import discounted" — চালাতে থাকুন, যতক্ষণ না সেটা আলাদাভাবে কাজ করে।
no tests ran in 0.01s pytest চলেছে, কিন্তু কিছুই খুঁজে পায়নি। হয় ফাইলের নাম test_ দিয়ে শুরু হয়নি (tests_prices.py, prices_tests.py আর test.py — সবগুলোই বাদ পড়ে), নয়তো ফাংশনের নাম test দিয়ে শুরু হয়নি (check_quarter_off কোনো সাড়াশব্দ ছাড়াই উপেক্ষিত হয়)। এখানে এক্সিট কোড 5, 0 নয়, যাতে স্বয়ংক্রিয় রান "কিছু পাওয়া যায়নি"-কে "সব পাস করেছে" বলে ভুল না করে।
যে টেস্ট স্পষ্টতই ব্যর্থ হওয়া উচিত, সেটা পাস করছে দেখুন কোথাও assert বাদ পড়েছে কি না:
from prices import discounted
def test_quarter_off():
discounted(80, 25) == 999$ pytest -q
. [100%]
1 passed in 0.01sতুলনাটা হিসাব করা হয়, তারপর ফেলে দেওয়া হয়। একটি টেস্ট তখনই ব্যর্থ হয় যখন তার ভেতরের কিছু একটা ত্রুটি তোলে (raise করে); assert না থাকলে কিছুই তোলে না।
PytestReturnNotNoneWarning: Test functions should return None আপনি assert ...-এর বদলে return discounted(99, 0) == 99 লিখেছেন। টেস্ট কী ফেরত দেয়, pytest তা উপেক্ষা করে, তাই তুলনাটা সত্য হোক বা মিথ্যা, এই টেস্ট পাস করবে। সতর্কবার্তাটি নিজেই জিজ্ঞেস করে: Did you mean to use assert instead of return?
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
এই ফাইলটি test_double.py নামে রেখে pytest -q চালানো হলো। আউটপুটের প্রথম লাইনের শুরুতে কোন অক্ষরগুলো থাকবে?
def double(n):
return n * 2
def test_two():
assert double(2) == 4
def test_zero():
assert double(0) == 1
def test_negative():
assert double(-3) == -6- AF..
- B..F
- C.F.
- DF
999 স্পষ্টতই ভুল উত্তর, অথচ pytest বলছে 1 passed। কেন?
def discounted(price, percent):
return price - price * percent / 100
def test_quarter_off():
discounted(80, 25) == 999- Apytest শুধু দেখে ফাংশনটি চলেছে কি না, ফলাফল দেখে না
- B`==` দিয়ে দশমিক সংখ্যা তুলনা করা যায় না
- Cফাংশনের নাম `test_` দিয়ে শুরু হয়নি
- Dলাইনটিতে `assert` নেই — তুলনার ফল তৈরি হয়ে ফেলে দেওয়া হচ্ছে
সঠিক টেস্টসহ একটি ফাইলের নাম double_checks.py। ফোল্ডারে pytest চালালে কী হবে?
- Aসব টেস্ট চলবে, কারণ ফাংশনের নাম `test_` দিয়ে শুরু
- B`no tests ran` — ফাইলের নাম `test_` দিয়ে শুরু হয়নি, তাই খোঁজাই হয়নি; exit code `5`
- C`ImportError` দিয়ে pytest থেমে যাবে
- Dpytest ফাইলটির নাম বদলে `test_double_checks.py` করে দেবে
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
একটি ফোল্ডার বানান, তাতে temperature.py নামের একটি ফাইলে দুটি ফাংশন রাখুন:
c_to_f(celsius)— ফেরত দেয়celsius * 9 / 5 + 32f_to_c(fahrenheit)— ফেরত দেয়(fahrenheit - 32) * 5 / 9
তারপর সেই ফোল্ডারের এনভায়রনমেন্টে pytest ইনস্টল করুন আর pytest --version দিয়ে নিশ্চিত হোন।
test_temperature.py লিখুন, কমপক্ষে পাঁচটি টেস্টসহ। পানি জমে যাওয়ার বিন্দু (0 আর 32), পানি ফোটার বিন্দু (100 আর 212), আর -40 রাখুন — শেষেরটি দুই স্কেলেই একই। প্রতিটি টেস্টকে এমন নাম দিন যা বলে দেয় সে কী যাচাই করছে।
তিনভাবে চালান — pytest, pytest -q আর pytest -v — আর সেগুলোর একটির পর এক্সিট কোড দেখুন।
তারপর ইচ্ছে করে ভাঙুন: c_to_f-এ 9 / 5 বদলে 5 / 9 করুন। চালানোর আগে অনুমান করুন কোন টেস্টগুলো ব্যর্থ হবে আর অগ্রগতির লাইনটি দেখতে কেমন হবে। pytest -q চালান, মিলিয়ে দেখুন, আর আবার এক্সিট কোড দেখুন।
সবশেষে test_temperature.py-এর নাম বদলে temperature_check.py করুন আর pytest চালান। সে কী বলে আর এক্সিট কোড কত, পড়ুন। তারপর নামটা আগের মতো করে দিন। "কিছুই পাওয়া যায়নি" দেখতে কেমন, সেটা জানা থাকলে একটা বিভ্রান্ত বিকেল বেঁচে যাবে।
সমাধান
কোড লেখার আগে প্রথমে পরিকল্পনা — আগের মতোই একটি টেবিল:
| ঘটনা | ইনপুট | প্রত্যাশিত | | --- | --- | --- | | পানি জমে যায় | c_to_f(0), f_to_c(32) | 32, 0 | | পানি ফোটে | c_to_f(100), f_to_c(212) | 212, 100 | | দুই স্কেল যেখানে মেলে | c_to_f(-40), f_to_c(-40) | -40, -40 |
temperature.py:
def c_to_f(celsius):
return celsius * 9 / 5 + 32
def f_to_c(fahrenheit):
return (fahrenheit - 32) * 5 / 9টেস্ট লেখার আগে, প্রজেক্ট ফোল্ডার থেকে ইমপোর্ট যাচাই:
$ python -c "import temperature; print('importable')"
importabletest_temperature.py:
from temperature import c_to_f, f_to_c
def test_water_freezes_at_32_f():
assert c_to_f(0) == 32
def test_water_boils_at_212_f():
assert c_to_f(100) == 212
def test_32_f_is_zero_c():
assert f_to_c(32) == 0
def test_212_f_is_100_c():
assert f_to_c(212) == 100
def test_minus_40_is_the_same_in_both_scales():
assert c_to_f(-40) == -40
assert f_to_c(-40) == -40$ pytest -q
..... [100%]
5 passed in 0.01s
$ echo $?
0
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/temperature
collecting ... collected 5 items
test_temperature.py::test_water_freezes_at_32_f PASSED [ 20%]
test_temperature.py::test_water_boils_at_212_f PASSED [ 40%]
test_temperature.py::test_32_f_is_zero_c PASSED [ 60%]
test_temperature.py::test_212_f_is_100_c PASSED [ 80%]
test_temperature.py::test_minus_40_is_the_same_in_both_scales PASSED [100%]
============================== 5 passed in 0.01s ===============================এবার ইচ্ছাকৃত বাগটি, c_to_f-এ 9 / 5 বদলে 5 / 9:
$ pytest -q
.F..F [100%]
=================================== FAILURES ===================================
__________________________ test_water_boils_at_212_f ___________________________
def test_water_boils_at_212_f():
> assert c_to_f(100) == 212
E assert 87.55555555555556 == 212
E + where 87.55555555555556 = c_to_f(100)
test_temperature.py:9: AssertionError
___________________ test_minus_40_is_the_same_in_both_scales ___________________
def test_minus_40_is_the_same_in_both_scales():
> assert c_to_f(-40) == -40
E assert 9.777777777777779 == -40
E + where 9.777777777777779 = c_to_f(-40)
test_temperature.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_temperature.py::test_water_boils_at_212_f - assert 87.55555555555...
FAILED test_temperature.py::test_minus_40_is_the_same_in_both_scales - assert...
2 failed, 3 passed in 0.01s
$ echo $?
1আর নাম বদলানো ফাইলটি:
$ pytest -q
no tests ran in 0.01s
$ echo $?
5কেন এভাবে লেখা হলো:
- প্রতি টেস্টে একটি আচরণ, নাম একটি বাক্যের মতো।
-vতালিকায় প্রতিটি লাইন বলে দেয় কী প্রতিশ্রুতি দেওয়া হয়েছে।test_water_boils_at_212_fব্যর্থ হলে আর কিছু পড়ার আগেই বুঝবেন কী ভেঙেছে। - জানা, যাচাইযোগ্য মান।
0/32,100/212আর-40এমন তথ্য যা ক্যালকুলেটর ছাড়াই যাচাই করা যায়, তাই ব্যর্থতা আঙুল তোলে কোডের দিকে, সন্দেহজনক কোনো প্রত্যাশিত মানের দিকে নয়। - দুই দিকই।
c_to_fআরf_to_cআলাদা কোড, তাই প্রতিটির নিজস্ব টেস্ট দরকার; একটি ঠিক হলে অন্যটি সম্পর্কে কিছুই বলা যায় না। - বাগসহ রানটিই সবচেয়ে বেশি কিছু বলে। মাত্র দুটি টেস্ট ব্যর্থ হয়েছে, আর
test_water_freezes_at_32_fতার একটি নয়:0 * 5 / 9 + 32তখনো32, কারণ শূন্যকে গুণ করলে যেকোনো গুণক আড়াল হয়ে যায়। ঠিক এই কারণেই পরিকল্পনায় একাধিক ঘটনা রাখা — শুধু হিমাঙ্কের টেস্ট থাকলে বাগ থাকা অবস্থাতেও স্যুটটি পাস করত।-40-এর টেস্টে দুটিassertলাইন আছে; প্রথমটি ব্যর্থ হয়েছে, তাই দ্বিতীয়টি আর চলেনি। এখানে সেটা মেনে নেওয়া যায়, কারণ দুটোই একই প্রতিশ্রুতির কথা বলে। - এক্সিট কোড —
0,1,5— এই তিনটির সাথেই সবচেয়ে বেশি দেখা হবে: সব পাস করেছে, কিছু ব্যর্থ হয়েছে, কিছুই পাওয়া যায়নি।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন