অধ্যায় 01

পাইটেস্ট ইনস্টল ও প্রথম টেস্ট

হাতে চালিয়ে দেখা কেন যথেষ্ট নয়, খালি assert কেন প্রায় কিছুই বলে না, আর pytest ইনস্টল করে প্রথম টেস্ট ফাইল লেখা ও চালানো। ডট, সারাংশ লাইন, -q, -v আর exit code পড়াসহ।

40 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

এই যে একটি ছোট ফাংশন, আর এ রকম ফাংশন আমরা বেশিরভাগই যেভাবে যাচাই করি — চালিয়ে দেখি কী বের হয়:

python
def discounted(price, percent):
    return price - price * percent / 100


print(discounted(200, 10))
print(discounted(80, 25))
print(discounted(99, 0))
text
180.0
60.0
99.0

তিনটি সংখ্যা পড়লেন, মনে মনে হিসাব কষলেন, আর ঠিক করলেন যে ফলগুলো ঠিক আছে। এটাও একটা টেস্ট। তবে হাতে-করা টেস্ট, আর এর দুটি সমস্যা আছে।

প্রথম সমস্যা, এটা বড় হতে পারে না। তিনটি সংখ্যা চোখে দেখে মেলানো সহজ। কিন্তু ত্রিশটি ফাংশন, প্রতিটির পাঁচটি করে ঘটনা, প্রতিবার কোড বদলানোর পর আবার মেলানো — সেটা সহজ নয়, আর কেউ তা করেও না।

দ্বিতীয় সমস্যাটা আরও খারাপ। এক মাস পরে কেউ ফাংশনটিকে একটু গুছিয়ে লিখল:

python
def discounted(price, percent):
    return price * (1 - percent // 100)
text
$ python -c "from prices import discounted; print(discounted(200, 10))"
200

দেখতে যুক্তিসংগত, আর কোনো ত্রুটি ছাড়াই চলে। কিন্তু // হলো পূর্ণসংখ্যার ভাগ, তাই 10 // 100 হয় 0, আর সব ছাড় নিঃশব্দে উধাও হয়ে গেছে। যে কোড আগে কাজ করত কিন্তু এখন আর করে না, তাকে বলে রিগ্রেশন (regression), আর হাতে-করা যাচাই ঠিক এটাই ধরতে পারে না — কারণ পুরোনো সংখ্যাগুলো আবার মিলিয়ে দেখতে কেউ ফিরে যায় না।

সমাধান হলো প্রত্যাশিত উত্তরগুলো কোডে লিখে রাখা, যাতে একটি যন্ত্র প্রতিবার সেগুলো যাচাই করতে পারে। এর জন্য পাইথনে আগে থেকেই একটি স্টেটমেন্ট আছে, assert। যাচাইগুলো prices.py-এর পাশে check_prices.py নামের একটি ফাইলে রাখুন:

python
from prices import discounted

assert discounted(200, 10) == 180.0
assert discounted(80, 25) == 60.0
assert discounted(99, 0) == 99
print("all good")

আসল ফাংশনটি দিয়ে চালালে:

text
$ python check_prices.py
all good

আর "গোছানো" ফাংশনটি দিয়ে:

text
$ 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 থাকতে হবে, আর একটি ফাংশন লিখতে জানতে হবে।


টেস্ট লেখার আগে

আগের অধ্যায় ছিল কী টেস্ট করবেন তা ঠিক করা নিয়ে। এই অধ্যায় সেই বাস্তব জিনিসগুলো নিয়ে, যেগুলো টেস্ট কোডের প্রথম লাইন চলার আগেই জায়গামতো থাকতে হবে। জিনিস চারটি, আর প্রতিটি না থাকলে নিজস্ব একটা বিভ্রান্তিকর ত্রুটি দেখা দেয় — তাই ক্রম ধরে একে একে যাচাই করে নেওয়াই লাভজনক।

১. একটি প্রজেক্ট ফোল্ডার, একটি এনভায়রনমেন্ট। এই অধ্যায়ের সবকিছু একটিমাত্র ফোল্ডারে থাকে:

text
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। তাই কোনো টেস্ট লেখার আগে প্রজেক্ট ফোল্ডার থেকে ঠিক এই ইমপোর্টটি কাজ করে কি না দেখে নিন:

text
$ 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 ব্যবহার করে, তাহলে প্রজেক্ট ফোল্ডারে এটা চালান:

text
$ 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.1

uv এটাকে pyproject.toml-এ একটি dev গ্রুপের নিচে লিখে রাখে, ফলে পরে যে-ই প্রজেক্টটি সেট আপ করুক, একই টুল পাবে:

toml
[dependency-groups]
dev = [
    "pytest>=9.1.1",
]

আর যদি ভার্চুয়াল এনভায়রনমেন্টের ভেতরে সাধারণ pip ব্যবহার করেন, তাহলে এনভায়রনমেন্টটি চালু (activate) করে এটা চালান:

text
$ python -m pip install pytest

pip ডাউনলোডের অগ্রগতি দেখায়, আর শেষ করে এভাবে:

text
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 লিখলে নিশ্চিত হয় যে প্যাকেজটি ঠিক সেই পাইথনেই বসছে যেটা আপনি চালাবেন — "ইনস্টল তো করলাম, কিন্তু পাচ্ছি না" সমস্যার এটা একটা সাধারণ কারণ।

এবার দেখে নিন কাজ হয়েছে কি না:

text
$ 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 বানান:

python
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) == 99

check_prices.py-এর প্রতিটি যাচাই এখন নিজস্ব একটি ছোট ফাংশন, আর প্রতিটির নামই বলে দেয় কী যাচাই হচ্ছে। কোনো print নেই, টেস্টের কোনো তালিকা নথিভুক্ত করতে হয় না, কোনো if __name__ == "__main__" নেই। এবার প্রজেক্ট ফোল্ডার থেকে pytest চালান:

text
$ 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 দিয়ে, যার সাথে একটু পরেই ভালোভাবে পরিচয় হবে:

text
$ 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, চুপচাপ) শিরোনামের অংশটা বাদ দেয়:

text
$ pytest -q
...                                                                      [100%]
3 passed in 0.01s

-v (verbose, বিস্তারিত) প্রতিটি টেস্টকে তার পুরো নামসহ আলাদা লাইন দেয়:

text
$ 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-এর বানানো একটি ফোল্ডার, যেখানে সে এক রান থেকে আরেক রানে কিছু তথ্য মনে রাখে, যেমন গতবার কোন টেস্টগুলো ব্যর্থ হয়েছিল। এটা মুছে ফেলা নিরাপদ, আর ভার্সন কন্ট্রোলে এর জায়গা নেই।

এক্সিট কোড: যন্ত্র কীভাবে ফল পড়ে

আপনি সারাংশ লাইন পড়েন। কিন্তু একটি স্ক্রিপ্ট, এডিটর বা বিল্ড সার্ভার তা পারে না — সে পড়ে এক্সিট কোড, অর্থাৎ যে সংখ্যাটি প্রতিটি প্রোগ্রাম শেষ হওয়ার সময় শেলকে ফেরত দেয়। ইউনিক্স শেলে শেষ এক্সিট কোডটি থাকে $?-তে:

text
$ pytest -q
...                                                                      [100%]
3 passed in 0.01s
$ echo $?
0

আর ভাঙা ফাংশনটি দিয়ে, যে রানটি 2 failed, 1 passed-এ শেষ হয়েছিল তার পরে:

text
$ echo $?
1
  • 0 — সবকিছু খুঁজে পাওয়া গেছে, সবকিছু পাস করেছে
  • 1 — টেস্ট চলেছে, আর অন্তত একটি ব্যর্থ হয়েছে

(PowerShell-এ ভেরিয়েবলটির নাম $LASTEXITCODE।) এই কারণেই কোড মার্জ করার আগে টেস্ট স্বয়ংক্রিয়ভাবে চালানো যায়: শূন্য ছাড়া অন্য কোনো এক্সিট কোড প্রক্রিয়াটি থামিয়ে দেয়। আরও কয়েকটি কোড আছে; তার একটির সাথে নিচে "কিছু ভাঙা অবস্থা ও তার সমাধান" অংশে দেখা হবে।

টেস্ট ফাইল কোথায় থাকে

এই অধ্যায়ে টেস্ট ফাইলটি কোডের ঠিক পাশেই থাকছে:

text
shop/
├── prices.py
└── test_prices.py

হাতে গোনা কয়েকটি ফাইলের জন্য এটা ঠিক আছে। তবে বেশিরভাগ প্রজেক্ট টেস্টগুলো আলাদা একটি tests/ ফোল্ডারে জড়ো করে রাখে। যেভাবেই রাখুন, pytest নাম দেখেই সেগুলো খুঁজে পায়; একটি সত্যিকারের প্রজেক্ট কীভাবে সাজাতে হয়, আর তাতে কী বদলায়, সেটা তৃতীয় অধ্যায়ের বিষয়।


একটা সম্পূর্ণ উদাহরণ

একটি ফাংশন, যা নম্বরকে লেটার গ্রেডে পরিণত করে, grades.py:

python
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:

python
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"
text
$ 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 করুন — খুব সহজেই হয়ে যাওয়া একটা ভুল — আর চুপচাপ রূপটি চালান:

text
$ 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 ...:

text
$ 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 বাদ পড়েছে কি না:

python
from prices import discounted


def test_quarter_off():
    discounted(80, 25) == 999
text
$ 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?