কনফিগারেশন আর সতর্কবার্তা — নিয়মগুলো একটি ফাইলে
প্রতিবার একই ফ্ল্যাগ টাইপ না করে pyproject.toml বা pytest.ini-তে প্রজেক্টের নিয়ম লিখে রাখা — testpaths, addopts, pythonpath, markers, xfail_strict, log_cli — আর pytest.warns, deprecated_call ও filterwarnings দিয়ে সতর্কবার্তা পরীক্ষা ও নিয়ন্ত্রণ।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
shop প্রজেক্টটি এখন সেই চেহারা নিয়েছে যা বেশিরভাগ বাস্তব প্রজেক্টের থাকে: কোড src/-এ, টেস্ট tests/-এ।
shop/
├── pyproject.toml
├── src/
│ └── shop/
│ ├── __init__.py
│ └── pricing.py
└── tests/
└── test_pricing.pyএকে ঠিকভাবে চালাতে আপনি শিখেছেন, আর «ঠিকভাবে» এখন একটি লম্বা লাইন হয়ে দাঁড়িয়েছে:
PYTHONPATH=src pytest -ra --strict-markers testsএকজন সহকর্মী রিপোজিটরিটি clone করে সেটাই টাইপ করেন যা যে কেউ করত:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 0 items / 1 error
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_pricing.py ____________________
ImportError while importing test module '/home/you/shop/tests/test_pricing.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)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_pricing.py:1: in <module>
from shop.pricing import line_total
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR tests/test_pricing.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.01s ===============================কোডে কোনো ভুল নেই। ভুলটা হলো, টেস্ট চালানোর নিয়মটি প্রজেক্টের ভেতরে নেই — আছে আপনার মাথায় আর shell history-তে। CI চালাবে তৃতীয় আরেকটি রূপ, আর একই commit থেকে তিনজন মানুষ তিন রকম ফল দেখবেন।
ওই flag-গুলো আপনার ব্যক্তিগত পছন্দ নয়; সেগুলো প্রজেক্টের নিয়ম। এই অধ্যায় সেগুলোকে এমন একটি ফাইলে সরিয়ে নেয় যা pytest প্রতিবার পড়ে — তারপর সেই একই ফাইল দিয়ে দ্বিতীয়, আরও নীরব একটি সমস্যার মীমাংসা করে: যে warning-গুলো স্ক্রিন দিয়ে গড়িয়ে চলে যায়, কেউ কখনো পড়ে না।
এই অধ্যায় শেষে আপনি পারবেন
- pytest-এর সেটিং
pyproject.toml-এ (বাpytest.ini-তে) রাখতে, আর header দেখে নিশ্চিত হতে কোন ফাইলটি ব্যবহার হয়েছে testpaths,addopts,pythonpath,minversion,markersআরxfail_strictভেবেচিন্তে বেছে নিতে, আর কেন — তা বলতে- কোনো run চোখে চোখে দেখতে চাইলে
log_cliদিয়ে live logging চালু করতে pytest.warnsআরpytest.deprecated_callদিয়ে assert করতে যে কোড warning দেয়,matchআর রেকর্ড করা তালিকা ব্যবহার করে- config-এ
filterwarningsদিয়ে, mark হিসেবে আর-Wদিয়ে warning নিয়ন্ত্রণ করতে — আর CI-তে সেগুলোকে error বানাতে
আগে যা জানা লাগবে: মার্কার, skip আর xfail।
টেস্ট লেখার আগে
এই অধ্যায়ে অন্য সময়ের চেয়ে টেস্ট কম লেখা হবে, আর বেশি লেখা হবে টেস্ট কীভাবে চলে তার নিয়ম। তবে আগের ভাবনাটা একই: কী প্রতিশ্রুতি দেওয়া হচ্ছে তা ঠিক করুন, তারপর সেই প্রতিশ্রুতি যাচাই করার কেসগুলো পরিকল্পনা করুন।
কী প্রতিশ্রুতি দেওয়া হচ্ছে। এখানে দুই ধরনের প্রতিশ্রুতি আছে।
- Suite কীভাবে চলে। যে কেউ শুধু
pytestটাইপ করলে CI-এর মতোই একই টেস্ট, একই import path আর একই কড়াকড়ি পাবেন। বানান-ভুল marker run থামিয়ে দেয়। যেxfailপাস করতে শুরু করে, সেটি জানানো হয়, লুকানো হয় না। - Return value ছাড়াও কোড যা বলে।
apply_discount(total, percent)একটি সংখ্যা ফেরত দেয়, আরpercent > 50হলে সাথে একটিUserWarning-ও দেয়, যেখানে বলা থাকে ছাড়টি "looks unusually large"।total_with_vat()এখনো কাজ করে, কিন্তু একটিDeprecationWarningদেয় যাline_total()-এর দিকে ইঙ্গিত করে। Warning-ও একরকম output; যখন সেটি চুক্তির অংশ, তখন output-এর মতোই তার টেস্ট হয়।
কী প্রস্তুত থাকতে হবে। pytest 9 সহ একটি virtual environment (pytest --version), টেস্ট থেকে import করা যায় এমন কোড (নিচের pythonpath সেটাই), আর প্রজেক্টের root-এ একটি config ফাইল। বাড়তি কোনো plugin লাগবে না।
warning-এর আচরণের পরিকল্পনা:
| কেস | ইনপুট | প্রত্যাশিত | | --- | --- | --- | | স্বাভাবিক পথ | apply_discount(200.0, 10) | 180.0, কোনো warning নেই | | সীমানা, এখনো নীরব | apply_discount(200.0, 50) | 100.0, কোনো warning নেই | | সীমানার ঠিক পরে | apply_discount(200.0, 60) | 80.0 আর একটি UserWarning, যাতে "unusually large" আছে | | deprecated নাম | total_with_vat(100.0, 1) | 115.0 আর একটি DeprecationWarning, যাতে line_total আছে | | কোনো library-র নিজস্ব deprecation | যা কিছু সেটি ঘটায় | আমাদের suite ফেল করায় না |
Configuration-এর ক্ষেত্রে «টেস্ট» হলো একটি run: root থেকে আর tests/-এর ভেতর থেকে — দুই জায়গাতেই শুধু pytest চালালে কেবল tests/ collect হতে হবে আর পাস করতে হবে, আর বানান-ভুল marker run থামিয়ে দিতে হবে।
কী টেস্ট করবেন না। পাইথনের warnings মডিউল কাজ করে কি না, বা pytest তার config পড়ে কি না — সেটি অন্য কারও কোড, যার টেস্ট আগেই হয়েছে। warning-এর পুরো লেখাও নয়: যে শব্দগুচ্ছ দেখে পাঠক পদক্ষেপ নেন ("unusually large"), শুধু সেটি মেলান, যাতে বাকিটা নতুন করে লিখলেও কিছু না ভাঙে। আর তৃতীয় পক্ষের library-র deprecation-ও নয় — সেগুলো filter করা হয়, আর সেটি configuration-এর সিদ্ধান্ত, টেস্ট নয়।
সীমানার সারিটিই মানুষ ভুলে যায়, কারণ warning না থাকলে কোনো শব্দ হয় না। অধ্যায়ের শেষে config-এর একটি লাইন এটিকে এমন একটি যাচাইয়ে পরিণত করবে যা আপনি বিনা খরচে পান।
একটি ফাইল, যা সবকিছুর আগে পড়া হয়
একটি টেস্টও collect করার আগে pytest তার সেটিং পড়ে নেয়। সাধারণত এর ঠিকানা প্রজেক্টে আগে থেকেই থাকা pyproject.toml, [tool.pytest.ini_options] নামের একটি table-এ:
[project]
name = "shop"
version = "0.1.0"
[tool.pytest.ini_options]
pythonpath = ["src"]pythonpath-এ থাকে কয়েকটি ডিরেক্টরি, প্রজেক্টের root-এর সাপেক্ষে, যেগুলো pytest টেস্ট import করার আগে sys.path-এ যোগ করে। যে কোডের টেস্ট হচ্ছে, সেটি সাধারণ:
def line_total(price, quantity):
if quantity < 1:
raise ValueError("quantity must be at least 1")
return round(price * quantity * 1.15, 2)$ PYTHONPATH=src python -c "from shop.pricing import line_total; print(line_total(15.0, 3))"
51.75
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
collected 2 items
tests/test_pricing.py .. [100%]
============================== 2 passed in 0.01s ===============================header-টি পড়ুন। configfile: pyproject.toml হলো pytest-এর জানানো যে সে কোন ফাইল ব্যবহার করেছে। যখনই মনে হবে সেটিং উপেক্ষা করা হচ্ছে, আগে এই লাইনটি দেখুন।
import-টা টেস্ট ফাইলের ভেতরেই ঠিক করে দিলে কী সমস্যা? এটিই সবচেয়ে চোখে-পড়া ভুল পথ, আর এটি কাজও করে — মাঝে মাঝে:
import sys
sys.path.insert(0, "src") # relative to wherever pytest was started
from shop.pricing import line_total
def test_three_items():
assert line_total(15.0, 3) == 51.75$ pytest -q
. [100%]
1 passed in 0.01s
$ cd tests && pytest -q
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR test_pricing.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sওখানে "src" বর্তমান ডিরেক্টরির সাপেক্ষে, তাই আপনি কোথায় দাঁড়িয়ে আছেন তার উপর নির্ভর করে টেস্টটি পাস করে বা ভাঙে। config-এর pythonpath প্রজেক্টের root-এর সাপেক্ষে, যা নড়ে না। import-এর ব্যবস্থা প্রজেক্টের বিষয়, তাই সেটি একবারই প্রজেক্টের ফাইলে যায়, প্রতিটি টেস্ট মডিউলের মাথায় নয়।
কোন ফাইল জেতে, আর root কোথায়
pytest কয়েকটি ফাইল মেনে নেয়। প্রতিটি ডিরেক্টরিতে সে এই ক্রমে খোঁজে: pytest.toml, .pytest.toml, pytest.ini, .pytest.ini, pyproject.toml, tox.ini, setup.cfg। শুরু করে command line-এ দেওয়া path থেকে (বা বর্তমান ডিরেক্টরি থেকে), আর এগোয় উপরের দিকে; যে প্রথম ফাইলে সত্যিই একটি pytest section আছে, সেটিই জেতে। pytest table ছাড়া একটি pyproject.toml গোনায় ধরা হয় না।
সেই ডিরেক্টরিটিই হয় rootdir — header-এর rootdir: লাইন — আর config-এর relative path-গুলো তার সাপেক্ষে। তাই tests/-এর ভেতরে দাঁড়িয়েও root খুঁজে পাওয়া যায়:
$ cd tests && pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
collected 2 itemsএকই সেটিং pytest.ini-তে লিখতে হয় INI syntax-এ, একটি [pytest] section-এ, কোনো উদ্ধৃতিচিহ্ন বা বন্ধনী ছাড়া:
[pytest]
pythonpath = src
testpaths = tests
addopts = -ra --strict-markers
markers =
slow: takes more than a second; deselect with -m 'not slow'একটি ফাইল বেছে নিন। দুটোই থাকলে pytest.ini পুরোপুরি জেতে — কিছুই মেশানো হয় না — আর header সেটা বলে দেয়। এখানে pyproject.toml-এর পাশে দুই লাইনের একটি pytest.ini (minversion = 9.0) যোগ করা হয়েছে:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)
collected 0 items / 1 error…আর ModuleNotFoundError ফিরে এসেছে, কারণ pythonpath ছিল সেই ফাইলে যেটি এখন উপেক্ষিত।
testpaths — কিছু না বললে কোথায় খুঁজবে
কেউ rounding নিয়ে পরীক্ষা করতে গিয়ে একটি খসড়া ফাইল রেখে গেছেন, notes/test_scratch.py। শুধু pytest চালালে root-এর নিচের সবকিছু collect হয়, তাই এটিও চলে:
$ pytest -q --tb=no
F.. [100%]
=========================== short test summary info ============================
FAILED notes/test_scratch.py::test_try_rounding - assert 2.67 == 2.68
1 failed, 2 passed in 0.01stestpaths বলে দেয় আসল suite কোথায় থাকে:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]$ pytest -q
.. [100%]
2 passed in 0.01sপুরো header-এ এখন testpaths: tests-ও দেখতে পাবেন। এটি একটি default, বেড়া নয়: শুধু তখনই খাটে যখন আপনি root থেকে কোনো path না দিয়ে চালান, তাই pytest notes এখনো খসড়া ফাইলটি চালায়। বড় রিপোজিটরিতে এটি collection-ও দ্রুত করে, কারণ pytest আর docs/, build/ বা virtual environment-এর ফোল্ডারে ঘুরে বেড়ায় না।
addopts আর markers — যে নিয়ম সবাই পায়
addopts হলো এমন লেখা যা pytest আপনার টাইপ করা সবকিছুর আগে বসিয়ে দেয়। শুরুর অংশের প্রজেক্টের দুটি নিয়ম এখানেই যায়:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-ra --strict-markers"-ra প্রতিটি টেস্টের জন্য একটি ছোট সারাংশ-লাইন ছাপে যেটি সোজাসুজি পাস করেনি — skipped, xfailed, xpassed, failed — তাই কোনো skip কখনো চোখ এড়াতে পারে না। --strict-markers রেজিস্টার-না-করা marker-কে error বানায়। নতুন একটি ফাইল, tests/test_discounts.py, যাতে একটি টেস্ট @pytest.mark.slow দিয়ে চিহ্নিত (আর একটি Windows ছাড়া অন্য জায়গায় skip হয়), আর marker-টি এখনো রেজিস্টার করা হয়নি:
$ pytest -q
==================================== ERRORS ====================================
___________________ ERROR collecting tests/test_discounts.py ___________________
'slow' not found in `markers` configuration option
=========================== short test summary info ============================
ERROR tests/test_discounts.py - Failed: 'slow' not found in `markers` configu...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sflag-টির উদ্দেশ্যই এটা। এটি না থাকলে @pytest.mark.slwo-এর মতো একটি বানান-ভুল সারাংশে শুধু একটি PytestUnknownMarkWarning পায়, run সবুজই থাকে, আর -m "not slow" সেই টেস্টটি চালাতেই থাকে যেটি আপনি বাদ দিতে চেয়েছিলেন। marker রেজিস্টার করুন, প্রতিটির জন্য এক লাইন, এমন একটি বর্ণনাসহ যা pytest --markers দেখাবে:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]$ pytest -q
.s.. [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_discounts.py:14: Windows rounding check
3 passed, 1 skipped in 0.01s
$ pytest --markers
@pytest.mark.slow: takes more than a second; deselect with -m 'not slow'SKIPPED লাইনটি -ra-এর কাজ: এখন প্রতিটি run-এ একটি skip নিজেই তার কারণ জানায়।
addopts-এ যা থাকা উচিত নয়। ভুল পথটি হলো addopts = "-v -x --pdb"। ওগুলো আজকের debugging-এর জন্য আপনার অভ্যাস; শেয়ার করা ফাইলে রাখলে সেগুলো সবার উপর চাপে, CI-সহ — যেখানে --pdb এমন একটি কীবোর্ডের জন্য অনন্তকাল অপেক্ষা করে যার সামনে কেউ নেই। addopts-এ শুধু প্রজেক্টের নিয়ম রাখুন। আপনি যা টাইপ করেন তা এর পরে যোগ হয়, তাই pytest -x শুধু আপনার জন্য এখনো কাজ করে, আর -o এক run-এর জন্য যেকোনো সেটিং বদলে দেয়:
$ pytest -q -o addopts=""
.s.. [100%]
3 passed, 1 skipped in 0.01sএবার কোনো skip লাইন নেই, কারণ ওই run-এর জন্য -ra বাদ পড়েছে।
minversion আর xfail_strict
কারও pytest ফাইলটির জন্য খুব পুরোনো হলে minversion সঙ্গে সঙ্গে run থামিয়ে দেয়। এমন একটি version চাইলে যা এখনো নেই, বার্তাটি দেখা যায়:
$ pytest
ERROR: /home/you/mv/pytest.ini: 'minversion' requires pytest-10.0, actual pytest-9.1.1'শুরুতেই একটি স্পষ্ট error, পুরোনো একটি pytest সেটিং অর্ধেক বুঝে চলার চেয়ে ভালো।
xfail_strict গত অধ্যায়ের একটি খোলা প্রান্তের মীমাংসা করে: যে xfail পাস করতে শুরু করে। এখানে একটি বাগকে প্রত্যাশিত-ফেল হিসেবে চিহ্নিত করা হয়েছিল, আর তারপর কেউ সেটি ঠিক করে ফেলেছেন:
import pytest
def refund(amount):
return round(amount * 0.9, 2)
@pytest.mark.xfail(reason="bug #41: refunds lose a cent")
def test_refund_keeps_cents():
assert refund(10.0) == 9.0$ pytest -q
X [100%]
1 xpassed in 0.01sএকটি সবুজ run, একটি X যা কেউ পড়ে না, আর একটি marker যা এখন মিথ্যা বলছে। config-এ xfail_strict = true থাকলে:
$ pytest -q
F [100%]
=================================== FAILURES ===================================
___________________________ test_refund_keeps_cents ____________________________
[XPASS(strict)] bug #41: refunds lose a cent
=========================== short test summary info ============================
FAILED test_refunds.py::test_refund_keeps_cents - [XPASS(strict)] bug #41: re...
1 failed in 0.01sসুখবরটি এখন আসে একটি failure হিসেবে, যা আপনাকে marker-টি সরাতে বলে — এ কারণেই এটি টেস্ট ধরে ধরে নয়, গোটা প্রজেক্টের জন্য সেট করা সার্থক। pytest 9.1 নতুন নাম strict_xfail-ও মেনে নেয়, আর একটিমাত্র strict = true-ও, যা একবারে সব কড়াকড়ির অপশন চালু করে (markers আর xfail-সহ); xfail_strict আগের মতোই কাজ করে।
Live log: log_cli
ডিফল্টভাবে pytest log record ধরে রাখে আর শুধু ফেল করা টেস্টের জন্য দেখায়। একটি পাস করা run-এর পুরো কাহিনি দেখতে চাইলে live logging চালু করুন:
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"তখন প্রতিটি টেস্টের INFO আর তার উপরের record চলার সময়েই একটি live log call শিরোনামের নিচে ছাপা হয়; নিচের পূর্ণাঙ্গ উদাহরণে সেটি দেখা যাবে। Live log বেশ কোলাহলপূর্ণ, তাই অনেক প্রজেক্ট ফাইলে শুধু log_cli_level রাখে, আর দরকার হলে pytest -o log_cli=true দিয়ে output চালু করে।
pytest 9-এর native table: [tool.pytest]
[tool.pytest.ini_options]-এর অদ্ভুত নামটির একটি কারণ আছে: এটি একটি TOML ফাইলে বসে থাকা INI সেটিং, তাই প্রতিটি মান লেখায় রূপান্তরিত হয়। pytest 9 একটি native table যোগ করেছে, [tool.pytest], যেখানে মানগুলো তাদের TOML ধরন রাখে — আসল list, আসল boolean:
[tool.pytest]
minversion = "9.0"
pythonpath = ["src"]
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers"]
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
strict_xfail = true$ pytest -q
.s.. [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_discounts.py:14: Windows rounding check
3 passed, 1 skipped in 0.01saddopts এখন একটি list। দুটি নিয়ম: একটি table বা অন্যটি ব্যবহার করুন, একই ফাইলে কখনো দুটো নয় (pytest শুরু করতেই রাজি হয় না), আর মনে রাখুন 9-এর আগের pytest [tool.pytest] একেবারেই পড়ে না। এই অধ্যায় [tool.pytest.ini_options] রাখছে, কারণ আপনি যত pytest দেখতে পারেন তার সবগুলোতে এটি কাজ করে; যে প্রজেক্ট শুধু pytest 9 চালায়, সেখানে native table-ই সুপারিশকৃত রূপ। pytest 9 একই native ধাঁচে [pytest] table-সহ একটি আলাদা pytest.toml-ও পড়ে।
Cache, সংক্ষেপে
একটি run-এর পর root-এ একটি .pytest_cache/ ফোল্ডার থাকে। সেটি মনে রাখে গতবার কোন টেস্টগুলো ফেল করেছিল, আর এটিই --lf (last failed) আর --ff (failed first)-কে চালায়। দুটি flag জেনে রাখার মতো। pytest --cache-clear run-এর আগে এটি খালি করে — কাজে লাগে যখন --lf বারবার এমন টেস্ট বাছে যা আর নেই। pytest -p no:cacheprovider cache plugin-টি পুরোপুরি বন্ধ করে দেয়, ফলে কিছুই লেখা হয় না, যা read-only checkout-এর জন্য মানানসই। মূল্য হলো, plugin-এর অপশনগুলোও তার সাথে উধাও হয়ে যায়:
$ pytest -q -p no:cacheprovider --lf
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --lf
inifile: /home/you/mv/pytest.ini
rootdir: /home/you/mvWarning: run-এর একেবারে নিচের অংশ
warning হলো এমন একটি বার্তা যে কিছু একটা এখনো ভুল হয়নি। পাইথন সেটি ছাপে আর কাজ চালিয়ে যায়:
import warnings
def line_total(price, quantity):
return round(price * quantity * 1.15, 2)
def total_with_vat(price, quantity):
warnings.warn(
"total_with_vat() is deprecated; use line_total()",
DeprecationWarning,
stacklevel=2,
)
return line_total(price, quantity)
def apply_discount(total, percent):
if percent > 50:
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
return round(total * (100 - percent) / 100, 2)$ python -c "from pricing import apply_discount; print(apply_discount(200.0, 60))"
/home/you/wn/pricing.py:19: UserWarning: discount of 60% looks unusually large
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
80.0stacklevel=2 deprecation-কে caller-এর লাইনের দিকে নির্দেশ করায়, অর্থাৎ সেই লাইন যা কাউকে বদলাতে হবে। টেস্ট চলার সময় ওঠা warning-গুলো pytest ধরে ফেলে আর শেষে একটি অংশে জড়ো করে:
from pricing import total_with_vat
def test_old_name_still_works():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q
. [100%]
=============================== warnings summary ===============================
test_old_api.py::test_old_name_still_works
/home/you/wn/test_old_api.py:5: DeprecationWarning: total_with_vat() is deprecated; use line_total()
assert total_with_vat(100.0, 1) == 115.0
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 passed, 1 warning in 0.01sটেস্টটি পাস করেছে, আর শেষ লাইন বলছে 1 passed, 1 warning। বড় suite-এ এই অংশ বেড়ে কয়েক ডজন entry-তে দাঁড়ায়, সবাই এটি পেরিয়ে স্ক্রল করতে শিখে যায়, আর একদিন deprecated ফাংশনটি সরিয়ে ফেলা হয় আর «পাস করা» suite ভেঙে পড়ে। একটি warning নিয়ে আপনি দুটি কাজ করতে পারেন: যখন সেটি আচরণের অংশ, তখন সেটি প্রত্যাশা করা, আর যখন নয়, তখন সেটি প্রত্যাখ্যান করা।
pytest.warns — একটি warning প্রত্যাশা করা
যখন একটি warning চুক্তির অংশ — পরিকল্পনার «সীমানার ঠিক পরে» সারিটি — তখন এর টেস্ট সেভাবেই করুন যেভাবে pytest.raises দিয়ে exception-এর টেস্ট করেন:
import pytest
from pricing import apply_discount
def test_large_discount_warns():
with pytest.warns(UserWarning, match="unusually large"):
result = apply_discount(200.0, 60)
assert result == 80.0
def test_small_discount_is_quiet():
assert apply_discount(200.0, 10) == 180.0$ pytest -q test_discount.py
.. [100%]
2 passed in 0.01sblock-টি পাস করে যখন এর ভেতরে অন্তত একটি UserWarning (বা তার subclass) ওঠে এবং তার বার্তা match-এর সাথে মেলে — match একটি regular expression, যা লেখার ভেতরে খোঁজা হয়, ঠিক pytest.raises-এর মতো। দুটো অর্ধেকই জোরালোভাবে ফেল করে। এখানে কোড 50-এ নীরব, আর দ্বিতীয় টেস্টটি ভুল শব্দ খুঁজছে:
import pytest
from pricing import apply_discount
def test_fifty_percent_warns():
with pytest.warns(UserWarning):
apply_discount(200.0, 50)
def test_wrong_message():
with pytest.warns(UserWarning, match="too large"):
apply_discount(200.0, 60)$ pytest -q test_discount_bad.py
FF [100%]
=================================== FAILURES ===================================
___________________________ test_fifty_percent_warns ___________________________
def test_fifty_percent_warns():
> with pytest.warns(UserWarning):
E Failed: DID NOT WARN. No warnings of type (<class 'UserWarning'>,) were emitted.
E Emitted warnings: [].
test_discount_bad.py:7: Failed
______________________________ test_wrong_message ______________________________
def test_wrong_message():
> with pytest.warns(UserWarning, match="too large"):
E Failed: Regex pattern did not match any of the 1 warnings emitted.
E Regex: 'too large'
E Emitted warnings: [UserWarning('discount of 60% looks unusually large')].
test_discount_bad.py:12: Failed
=============================== warnings summary ===============================
test_discount_bad.py::test_wrong_message
/home/you/wn/pricing.py:19: UserWarning: discount of 60% looks unusually large
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED test_discount_bad.py::test_fifty_percent_warns - Failed: DID NOT WARN....
FAILED test_discount_bad.py::test_wrong_message - Failed: Regex pattern did n...
2 failed, 1 warning in 0.01sদুটি রিপোর্টেই সেই warning-গুলোর তালিকা থাকে যেগুলো সত্যিই উঠেছিল, যা সাধারণত টেস্ট ঠিক করার জন্য যথেষ্ট। যে warning মেলেনি, সেটিও গিলে ফেলা হয় না; সেটি warnings summary-তে চলে যায়।
কেন সবসময় match দেবেন। শুধু pytest.warns(UserWarning) হলো দুর্বল রূপ: block-এর যেকোনো জায়গা থেকে যেকোনো UserWarning এটিকে সন্তুষ্ট করে — এমনকি কোনো library থেকে আসা একটিও, যেটি আপনি সম্পর্কহীন কারণে ডাকছেন। আপনার আসল warning-টি চলে যাওয়ার পরেও টেস্টটি পাস করে যেতে পারে। একটি মেলানো শব্দগুচ্ছ টেস্টটিকে এই warning-এর সাথে বেঁধে দেয়।
কতগুলো warning উঠল, বা সেগুলো কী বলল, তা যাচাই করতে as দিয়ে রেকর্ডটি রেখে দিন:
import pytest
from pricing import apply_discount
def test_each_large_discount_warns_once():
with pytest.warns(UserWarning) as record:
apply_discount(100.0, 60)
apply_discount(100.0, 10)
apply_discount(100.0, 75)
assert len(record) == 2
print(record[0].message)
print(record[1].category.__name__, record[1].lineno)$ pytest -q -s test_record.py
discount of 60% looks unusually large
UserWarning 19
.
1 passed in 0.01srecord-এ block-এর ভেতরে ধরা পড়া প্রতিটি warning-এর তালিকা থাকে — এখানে দুটি, কারণ 10%-এর call নীরব। প্রতিটি entry-তে আছে .message, .category, .filename আর .lineno।
pytest.deprecated_call — আপনার ব্যবহারকারীদের প্রতি একটি প্রতিশ্রুতি
একটি ফাংশন deprecate করা মানে একটি প্রতিশ্রুতি: «এটি এখনো কাজ করে, আর আপনাকে জানানো হয়েছে»। pytest.deprecated_call() দুটো অর্ধেকই যাচাই করে:
import pytest
from pricing import total_with_vat
def test_old_name_still_works():
with pytest.deprecated_call():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q test_old_api.py
. [100%]
1 passed in 0.01sএটি আসলে pytest.warns, যেখানে category-গুলো আগে থেকেই বসানো — DeprecationWarning, PendingDeprecationWarning আর FutureWarning — আর এটিও match নেয়। warnings summary-র entry-টি এখন নেই: warning-টি প্রত্যাশিত ছিল, তাই সেটি খরচ হয়ে গেছে।
filterwarnings — warning প্রত্যাখ্যান করা
প্রত্যাশিত warning-গুলো এখন টেস্ট। বাকিগুলো হয় কোলাহল, নয়তো আগাম সতর্কসংকেত, আর CI-তে যে অভ্যাসটি গ্রহণ করার মতো তা হলো: প্রতিটি অপ্রত্যাশিত warning একটি error। এক লাইন:
[tool.pytest.ini_options]
filterwarnings = [
"error",
]এখানে একটি suite, যাতে একটি পরিষ্কার টেস্ট, একটি যা এখনো deprecated নামটি ডাকে, আর একটি যা এমন একটি library থেকে currency.rate() ডাকে যা আপনি বদলাতে পারেন না, আর সেটি নিজের একটি DeprecationWarning দেয়:
from currency import rate
from pricing import line_total, total_with_vat
def test_line_total():
assert line_total(100.0, 1) == 115.0
def test_old_name():
assert total_with_vat(100.0, 1) == 115.0
def test_rate():
assert rate("USD") == 1.0$ pytest -q --tb=short
.FF [100%]
=================================== FAILURES ===================================
________________________________ test_old_name _________________________________
test_shop.py:10: in test_old_name
assert total_with_vat(100.0, 1) == 115.0
^^^^^^^^^^^^^^^^^^^^^^^^
pricing.py:9: in total_with_vat
warnings.warn(
E DeprecationWarning: total_with_vat() is deprecated; use line_total()
__________________________________ test_rate ___________________________________
test_shop.py:14: in test_rate
assert rate("USD") == 1.0
^^^^^^^^^^^
currency.py:6: in rate
warnings.warn("rate() will need an API key from v3", DeprecationWarning)
E DeprecationWarning: rate() will need an API key from v3
=========================== short test summary info ============================
FAILED test_shop.py::test_old_name - DeprecationWarning: total_with_vat() is ...
FAILED test_shop.py::test_rate - DeprecationWarning: rate() will need an API ...
2 failed, 1 passed in 0.01sপ্রতিটি warning এখন একটি exception হিসেবে ওঠে, সাথে যে লাইন সেটি ঘটিয়েছে সেখান পর্যন্ত traceback। ঢালাও নীরবতার বদলে প্রতিটি failure একটি সিদ্ধান্ত পায়।
ভুল পথটি হলো "ignore::DeprecationWarning": এক লাইন, সব সবুজ, আর library-রটার সাথে আপনার নিজের deprecation-ও লুকিয়ে গেল। সঠিক পথটি হলো প্রতিটি ব্যতিক্রমকে সরু করা। একটি filter-এর আকার action:message:category:module — পাইথনের নিজের -W অপশনের মতোই — যেখানে message warning-এর লেখার শুরুর সাথে মেলে, আর module হলো সেই মডিউল যার নামে warning-টি ধরা হয়:
[tool.pytest.ini_options]
filterwarnings = [
"error",
"ignore::DeprecationWarning:currency",
]ক্রম গুরুত্বপূর্ণ: পরের লাইন অগ্রাধিকার পায়, তাই error যায় প্রথমে আর ব্যতিক্রমগুলো তার পরে। library-র warning এখন উপেক্ষা করা হয় শুধু তখনই যখন সেটি currency থেকে আসে। একটিমাত্র টেস্টের জন্য একই filter একটি mark হিসেবেও লাগানো যায় — এখানে test_old_name-এ, ফাইলের বাকিটা অপরিবর্তিত:
import pytest
from pricing import total_with_vat
@pytest.mark.filterwarnings("ignore:total_with_vat:DeprecationWarning")
def test_old_name():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q
... [100%]
3 passed in 0.01sওই mark শুধু warning-টিকে অনুমতি দেয়। যদি আপনার বক্তব্য হয় «এটিকে warning দিতেই হবে», তাহলে pytest.deprecated_call() বেশি জোরালো পছন্দ, কারণ warning উধাও হলে সেটি ফেলও করে। যে warning আপনি সহ্য করেন তার জন্য mark, আর যে warning-এর প্রতিশ্রুতি দেন তার জন্য warns।
একই filter এক run-এর জন্য -W দিয়েও দেওয়া যায়, যা কোনো নীতি লিখে ফেলার আগে পরখ করার জন্য সুবিধাজনক। আগের test_old_api.py-তে, কোনো config ছাড়াই:
$ pytest -q -W error::DeprecationWarning --tb=no -ra
F [100%]
=========================== short test summary info ============================
FAILED test_old_api.py::test_old_name_still_works - DeprecationWarning: total...
1 failed in 0.01sউৎসগুলো একমত না হলে সবচেয়ে নির্দিষ্টটি জেতে: টেস্টের উপরের mark -W-কে হারায়, আর -W config ফাইলকে হারায়। তিন-টেস্টের suite-এ -W config-এর currency-র জন্য দেওয়া ignore-কে অগ্রাহ্য করে, কিন্তু চিহ্নিত টেস্টটি তার ছাড় ধরে রাখে:
$ pytest -q -W error::DeprecationWarning --tb=no -ra
..F [100%]
=========================== short test summary info ============================
FAILED test_shop.py::test_rate - DeprecationWarning: rate() will need an API ...
1 failed, 2 passed in 0.01serror আপনাকে আরও একটি জিনিস এনে দেয়। পরিকল্পনার সীমানার সারিটির দিকে আবার তাকান, «50-এ কোনো warning নেই»। config-এ error থাকলে apply_discount(200.0, 50)-এর একটি সাধারণ টেস্ট ইতিমধ্যেই এটি যাচাই করে: ওই call কখনো warning দিলে সেটি একটি exception হতো আর টেস্টটি ফেল করত। নীরব কেসগুলোর জন্য বিশেষ কোনো কোড লাগে না।
একটি পূর্ণাঙ্গ উদাহরণ
সব নিয়ম এক জায়গায় নিয়ে shop প্রজেক্ট। pyproject.toml:
[project]
name = "shop"
version = "0.1.0"
[tool.pytest.ini_options]
minversion = "9.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
xfail_strict = true
filterwarnings = [
"error",
]
log_cli_level = "INFO"src/shop/pricing.py:
import logging
import warnings
log = logging.getLogger(__name__)
def line_total(price, quantity):
if quantity < 1:
raise ValueError("quantity must be at least 1")
return round(price * quantity * 1.15, 2)
def total_with_vat(price, quantity):
warnings.warn(
"total_with_vat() is deprecated; use line_total()",
DeprecationWarning,
stacklevel=2,
)
return line_total(price, quantity)
def apply_discount(total, percent):
if percent > 50:
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
log.info("discount %d%% on %.2f", percent, total)
return round(total * (100 - percent) / 100, 2)$ PYTHONPATH=src python -c "from shop.pricing import apply_discount; print(apply_discount(200.0, 10))"
180.0tests/test_pricing.py — অধ্যায়ের শুরুর পরিকল্পনা, সারি ধরে ধরে:
import pytest
from shop.pricing import apply_discount, line_total, total_with_vat
def test_line_total():
assert line_total(100.0, 1) == 115.0
def test_old_name_is_deprecated():
with pytest.deprecated_call(match="use line_total"):
assert total_with_vat(100.0, 1) == 115.0
def test_large_discount_warns():
with pytest.warns(UserWarning, match="unusually large") as record:
assert apply_discount(200.0, 60) == 80.0
assert len(record) == 1
def test_fifty_percent_is_quiet():
assert apply_discount(200.0, 50) == 100.0
@pytest.mark.slow
def test_many_lines():
total = sum(line_total(1.0, 1) for _ in range(10_000))
assert round(total, 2) == 11500.0
@pytest.mark.xfail(reason="bug #41: zero quantity should be free, not an error")
def test_zero_quantity():
assert line_total(15.0, 0) == 0.0$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/full
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_pricing.py .....x [100%]
=========================== short test summary info ============================
XFAIL tests/test_pricing.py::test_zero_quantity - bug #41: zero quantity should be free, not an error
========================= 5 passed, 1 xfailed in 0.01s =========================
$ pytest -m "not slow" -o log_cli=true
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/full
configfile: pyproject.toml
testpaths: tests
collected 6 items / 1 deselected / 5 selected
tests/test_pricing.py::test_line_total PASSED [ 20%]
tests/test_pricing.py::test_old_name_is_deprecated PASSED [ 40%]
tests/test_pricing.py::test_large_discount_warns
-------------------------------- live log call ---------------------------------
INFO shop.pricing:pricing.py:25 discount 60% on 200.00
PASSED [ 60%]
tests/test_pricing.py::test_fifty_percent_is_quiet
-------------------------------- live log call ---------------------------------
INFO shop.pricing:pricing.py:25 discount 50% on 200.00
PASSED [ 80%]
tests/test_pricing.py::test_zero_quantity XFAIL (bug #41: zero quant...) [100%]
=========================== short test summary info ============================
XFAIL tests/test_pricing.py::test_zero_quantity - bug #41: zero quantity should be free, not an error
================== 4 passed, 1 deselected, 1 xfailed in 0.01s ==================তিনটি জিনিস লক্ষ করার মতো।
প্রথমত, দুটি কমান্ডের কোনোটিতেই প্রজেক্টের কোনো নিয়ম নেই। নিয়মগুলো — টেস্ট কোথায়, কী import করা যায়, কড়া marker, কড়া xfail, warning মানে error — সবই ফাইলে, তাই যে সহকর্মী শুধু pytest টাইপ করেন তিনি হুবহু প্রথম run-টিই পান, আর CI-ও তাই পায়।
দ্বিতীয়ত, কোনো warnings summary নেই। কোড যে দুটি warning দেয়, দুটোই প্রত্যাশিত, তাই warns আর deprecated_call সেগুলো খরচ করে ফেলেছে; অন্য যেকোনো warning কোনো একটি টেস্ট ফেল করাত। একটি পরিষ্কার run এখন সত্যিই পরিষ্কার।
তৃতীয়ত, test_fifty_percent_is_quiet-এ warning-সংক্রান্ত কোনো কোড নেই, তবু এটি সীমানা পাহারা দেয়: error-এর অধীনে 50%-এ একটি warning উঠলে এটি ফেল করত।
যখন ভাঙে
ModuleNotFoundError: No module named 'shop' during collection টেস্ট থেকে কোডটি import করা যাচ্ছে না। config-এ pythonpath = ["src"] যোগ করুন, আর header-এর configfile: লাইনটি দেখে নিশ্চিত হোন যে পড়া হচ্ছে সেই ফাইলটিই।
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!) দুটি config ফাইল, আর আপনি যেটি সম্পাদনা করেছেন সেটি হেরেছে। কিছুই মেশানো হয় না। একটি মুছে ফেলুন।
'slow' not found in markers configuration option --strict-markers (বা strict = true) চালু আছে, আর marker-টি রেজিস্টার করা নেই বা বানান ভুল। markers-এর নিচে রেজিস্টার করুন, বা বানান ঠিক করুন।
PytestConfigWarning: Unknown config option: testpath একটি সেটিংয়ের বানান ভুল — testpaths-এর জায়গায় testpath। pytest শুধু warning দেয়, তাই সেটিংটি নীরবে কিছুই করে না। strict_config = true (বা strict = true) থাকলে এটি হয়ে যায় ERROR: Unknown config option: testpath আর run থেমে যায়, আপনি ঠিক সেটাই চান।
ERROR: ... Cannot use both [tool.pytest] (native TOML types) and [tool.pytest.ini_options] (string-based INI format) simultaneously. pyproject.toml-এ দুটো table-ই আছে। সবকিছু একটিতে সরিয়ে নিন।
TypeError: ... config option 'addopts' expects a list for type 'args', got str: '-ra --strict-markers' native [tool.pytest] table-এ addopts একটি list: addopts = ["-ra", "--strict-markers"]। string রূপটি [tool.pytest.ini_options]-এর জন্য।
ERROR: ... 'minversion' requires pytest-10.0, actual pytest-9.1.1' ইনস্টল করা pytest প্রজেক্টের প্রয়োজনের চেয়ে পুরোনো। virtual environment-এর ভেতরে এটি আপগ্রেড করুন।
Failed: DID NOT WARN. No warnings of type (<class 'UserWarning'>,) were emitted. block-এর ভেতরে ওই category-র কিছুই ওঠেনি। নিচের Emitted warnings: পড়ুন — প্রায়ই দেখা যায় কোড অন্য category দিয়ে warning দেয়, বা এই ইনপুটে দেয়ই না।
Failed: Regex pattern did not match any of the 1 warnings emitted. category ঠিক, লেখা ভুল। match একটি regular expression, তাই (, . আর ? escape করতে হয় — বা লেখাটিকে re.escape()-এ মুড়ে দিন।
AttributeError: module 'builtins' has no attribute 'DeprecatedWarning'. Did you mean: 'DeprecationWarning'? একটি filterwarnings লাইনের category-তে টাইপো। pytest শুরু করতেই রাজি হয় না, আর ঠিক উপরে সেই লাইনটি ছাপে যেটি সে parse করতে পারেনি।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
pytest -q -s দিয়ে চালানো হলো। টেস্টটি কোন সংখ্যাটি ছাপবে?
import warnings
import pytest
def check(n):
if n < 0:
warnings.warn("negative value", UserWarning)
return abs(n)
def test_record():
with pytest.warns(UserWarning) as record:
check(-1)
check(2)
check(-3)
print(len(record))- A3
- B1
- C2
- D0
প্রজেক্টের রুটে pytest.ini আর [tool.pytest.ini_options]-সহ pyproject.toml দুটোই আছে। pytest চালালে কী হয়?
- Aদুটো ফাইল মিলিয়ে নেওয়া হয়; ফাঁকগুলো `pyproject.toml` পূরণ করে
- B`pytest.ini` ব্যবহৃত হয়, `pyproject.toml`-এর সেটিং উপেক্ষিত হয়, আর হেডারে সতর্কবার্তা দেখায়
- C`pyproject.toml` জেতে, কারণ সেটি নতুন মানদণ্ড
- Dদুটি কনফিগ ফাইল থাকায় pytest ত্রুটি দিয়ে থেমে যায়
কনফিগে addopts = -ra --strict-markers আছে আর markers-এ শুধু slow নিবন্ধিত। pytest -m "not slow" চালালে কী হবে?
import pytest
@pytest.mark.slwo
def test_every_day_of_a_year():
fees = [day * 5 for day in range(366)]
assert fees[-1] == 1825- Aটেস্টটি ইচ্ছেমতো বাদ (deselect) পড়ে
- Bটেস্টটি চলে, সারাংশে একটি সতর্কবার্তাসহ
- Cটেস্টটি skip হয়
- Dকালেকশন থেমে যায়: 'slwo' not found in `markers` configuration option
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
library প্রজেক্ট দেরিতে বই ফেরতের জন্য জরিমানা নেয়। src/library/fees.py:
import warnings
PER_DAY = 5
def late_fee(days):
if days < 0:
raise ValueError("days cannot be negative")
if days > 60:
warnings.warn(f"{days} days late looks like a lost book", UserWarning)
return days * PER_DAY
def fine(days):
warnings.warn(
"fine() is deprecated; use late_fee()",
DeprecationWarning,
stacklevel=2,
)
return late_fee(days)$ PYTHONPATH=src python -c "from library.fees import late_fee; print(late_fee(4))"
20[tool.pytest.ini_options]table-টি এমনভাবে লিখুন যাতে root থেকে বাtests/-এর ভেতর থেকে শুধুpytestচালালেlibraryimport হয়, কেবলtests/collect হয়,-raসারাংশ ছাপা হয়, অজানা marker প্রত্যাখ্যাত হয়, অপ্রত্যাশিতভাবে পাস করাxfailফেল করে, আর প্রতিটি অপ্রত্যাশিত warning error হয়ে যায়। একটিslowmarker রেজিস্টার করুন।- আগে টেস্টের পরিকল্পনা লিখুন — কেস, ইনপুট, প্রত্যাশিত — যাতে থাকবে সময়মতো ফেরত, 60 দিনের সীমানা, 61 দিন, একটি ঋণাত্মক সংখ্যা আর deprecated
fine()। - পরিকল্পনা থেকে
tests/test_fees.pyলিখুন, সাথে একটিslowটেস্ট যা এক বছরের প্রতিটি দিন ঘুরে দেখে। pytestচালান, তারপরpytest -m "not slow", তারপরcd tests && pytest।
তারপর দুটি পরীক্ষা, প্রতিটির ফল আগে অনুমান করে নিন: deprecated_call টেস্টটির জায়গায় fine(3)-এর একটি সাধারণ call বসান, আর slow-এর বানান ভুল করে slwo লিখুন।
সমাধান
পরিকল্পনা:
| কেস | ইনপুট | প্রত্যাশিত | | --- | --- | --- | | সময়মতো ফেরত | late_fee(0) | 0, কোনো warning নেই | | সীমানা, এখনো নীরব | late_fee(60) | 300, কোনো warning নেই | | সীমানার ঠিক পরে | late_fee(61) | 305 আর একটি UserWarning, যাতে "lost book" আছে | | অবৈধ ইনপুট | late_fee(-1) | ValueError, যাতে "negative" আছে | | deprecated নাম | fine(3) | 15 আর একটি DeprecationWarning, যাতে late_fee আছে | | পুরো এক বছর (slow) | late_fee(0) … late_fee(365) | শেষ জরিমানা 1825, warning প্রত্যাশিত |
pyproject.toml:
[project]
name = "library"
version = "0.1.0"
[tool.pytest.ini_options]
minversion = "9.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
xfail_strict = true
filterwarnings = [
"error",
]tests/test_fees.py:
import pytest
from library.fees import fine, late_fee
def test_returned_on_time():
assert late_fee(0) == 0
def test_sixty_days_is_quiet():
# filterwarnings = error: any warning here would fail this test
assert late_fee(60) == 300
def test_sixty_one_days_warns():
with pytest.warns(UserWarning, match="lost book") as record:
assert late_fee(61) == 305
assert len(record) == 1
def test_negative_days_rejected():
with pytest.raises(ValueError, match="negative"):
late_fee(-1)
def test_fine_is_deprecated():
with pytest.deprecated_call(match="use late_fee"):
assert fine(3) == 15
@pytest.mark.slow
def test_every_day_of_a_year():
with pytest.warns(UserWarning):
fees = [late_fee(day) for day in range(366)]
assert fees[-1] == 1825$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/library
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_fees.py ...... [100%]
============================== 6 passed in 0.01s ===============================
$ pytest -m "not slow" -q
..... [100%]
5 passed, 1 deselected in 0.01s
$ cd tests && pytest -q
...... [100%]
6 passed in 0.01sদুটি পরীক্ষা:
$ pytest -q # test_fine_still_works calls fine(3) with no deprecated_call
=========================== short test summary info ============================
FAILED tests/test_fees.py::test_fine_still_works - DeprecationWarning: fine()...
1 failed, 5 passed in 0.01s
$ pytest -q # @pytest.mark.slwo
ERROR tests/test_fees.py - Failed: 'slwo' not found in `markers` configuratio...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sপ্রতিটি সিদ্ধান্তের কারণ:
pythonpathআরtestpathsফাইলে, command line-এ নয়। এ কারণেইcd tests && pytestএকই ছয়টি পাস দেয়: দুটোই rootdir-এর সাপেক্ষে, যা pytest উপরে উঠতে উঠতেpyproject.tomlপর্যন্ত গিয়ে খুঁজে পায়।addopts-এ শুধু নিয়ম।-raআর--strict-markersসবার জন্য একই নীতি;-vবা-xব্যক্তিগত অভ্যাস, সেগুলো আপনার নিজের command line-এই থাকে।- কোনো ব্যতিক্রম ছাড়া
filterwarnings = ["error"]। প্রজেক্টে এখনো তৃতীয় পক্ষের কোনো warning নেই, তাই ছাড় দেওয়ার কিছু নেই। যেদিন একটি দেখা দেবে, একটি সরুignore::...:moduleলাইন যোগ করবেন, ঢালাওignoreকখনো নয়। - 60 দিনের টেস্টে warning-সংক্রান্ত কোনো কোড নেই।
error-এর অধীনে 60 দিনে যেকোনো warning এটিকে ফেল করাত, তাই config-ই সীমানা পাহারা দেয়। - একটি নির্দিষ্ট বার্তাকে লক্ষ্য করা প্রতিটি যাচাইয়ে
match। "lost book" আর "use late_fee" হলো সেই শব্দ যা দেখে পাঠক পদক্ষেপ নেন; বাকিটা ইচ্ছেমতো নতুন করে লেখা যায়। পুরো বছরের টেস্টেmatchইচ্ছে করেই নেই: সেটি জরিমানার হিসাব নিয়ে, আর তার শুধু এটুকু বলা দরকার যে সেখানে warning প্রত্যাশিত। - 61 দিনের টেস্টে
record। এটি প্রমাণ করে যে একটি call একবারই warning দেয়, প্রতিটি ভেতরের ধাপে নয়। - পরীক্ষাগুলো।
deprecated_callছাড়া deprecation-টি অপ্রত্যাশিত, তাইerrorসেটিকে failure বানায় — কেউ পুরোনো API ডাকলে CI-এর ঠিক এটাই করা উচিত। বানান-ভুল marker collection থামিয়ে দেয়, কারণ--strict-markersআছেaddopts-এ। (এখানেerrorfilter-ও এটি ধরে ফেলত, কারণ অজানা marker একটিPytestUnknownMarkWarningদেয় — কিন্তু marker-এর নিয়ম warning-এর নীতির উপর নির্ভর করা উচিত নয়, তাই দুটোই থাকে।)
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন