Testing (pytest) ও Code Quality
Test, Ruff, type checker, pre-commit
AI এর যুগে test লেখা আগের চেয়েও জরুরি। আগে তুমি নিজে code লিখতে, তাই প্রতিটা লাইন জানতে। এখন AI ৫০ লাইন লিখে দেয় ১০ সেকেন্ডে। সেই code ঠিক কিনা প্রমাণ করার সবচেয়ে নির্ভরযোগ্য উপায় হলো test। আর পরে যখন AI কে বলবে "এই function টা দ্রুত করো", test বলে দেবে কিছু ভেঙেছে কিনা। এই অধ্যায়ে pytest দিয়ে test লেখা, আর Ruff, Pyright, pre-commit দিয়ে code এর মান ধরে রাখা শিখবো।
#প্রথম test
uv add --dev pytest# src/shop/pricing.py
def apply_discount(price: int, percent: int) -> int:
if not 0 <= percent <= 100:
raise ValueError("percent ০ থেকে ১০০ এর মধ্যে হতে হবে")
return price - price * percent // 100# tests/test_pricing.py
import pytest
from shop.pricing import apply_discount
def test_normal_discount():
assert apply_discount(1000, 10) == 900
def test_zero_discount():
assert apply_discount(1000, 0) == 1000
def test_full_discount():
assert apply_discount(1000, 100) == 0
def test_invalid_percent_raises():
with pytest.raises(ValueError, match="০ থেকে ১০০"):
apply_discount(1000, 150)uv run pytest # সব test
uv run pytest -v # প্রতিটা test এর নাম সহ
uv run pytest -x # প্রথম ব্যর্থ test এ থামো
uv run pytest -k discount # নামে "discount" আছে এমন testনিয়ম: test file এর নাম test_ দিয়ে শুরু, test function এর নামও test_ দিয়ে শুরু। pytest নিজেই খুঁজে নেয়।
#ভালো test এর গঠন: Arrange, Act, Assert
def test_cart_total_with_discount():
# Arrange: প্রস্তুতি
cart = Cart()
cart.add(Item("Burger", 25000), qty=2)
cart.add(Item("Coke", 4000))
# Act: আসল কাজ
total = cart.total(coupon="EID10")
# Assert: যাচাই
assert total == 48600প্রতিটা test একটা জিনিস যাচাই করবে, নাম দেখেই বোঝা যাবে কী যাচাই হচ্ছে। Test ব্যর্থ হলে নাম পড়েই বুঝবে কী ভেঙেছে।
#কী test করবে
শুধু "স্বাভাবিক" ক্ষেত্র না। Bug লুকিয়ে থাকে কিনারায়:
- সাধারণ ক্ষেত্র: ১০০০ টাকায় ১০% ছাড়
- সীমা (boundary): ০%, ১০০%, ঠিক ১০০০ টাকায় (যদি "১০০০ এর বেশি হলে ছাড়" নিয়ম থাকে)
- খালি: খালি cart, খালি string, খালি list
- ভুল input: ঋণাত্মক দাম, ১৫০% ছাড়, None
- বড় সংখ্যা: ১০ লাখ item
#parametrize: একই test অনেক data দিয়ে
@pytest.mark.parametrize(
("price", "percent", "expected"),
[
(1000, 10, 900),
(1000, 0, 1000),
(1000, 100, 0),
(999, 33, 670), # ভাগের পর পূর্ণসংখ্যা
(1, 50, 1), # ছোট সংখ্যায় rounding
],
)
def test_apply_discount(price, percent, expected):
assert apply_discount(price, percent) == expected#Fixture: বারবার লাগা প্রস্তুতি
# tests/conftest.py ← এই file এর fixture সব test file পায়
import pytest
from shop.cart import Cart, Item
@pytest.fixture
def burger() -> Item:
return Item("Burger", 25000)
@pytest.fixture
def cart_with_items(burger) -> Cart: # fixture অন্য fixture ব্যবহার করতে পারে
cart = Cart()
cart.add(burger, qty=2)
return cart
# tests/test_cart.py
def test_total(cart_with_items):
assert cart_with_items.total() == 50000
def test_remove(cart_with_items, burger):
cart_with_items.remove(burger)
assert cart_with_items.total() == 0প্রতিটা test নতুন fixture পায়, তাই এক test এর বদল আরেক test কে প্রভাবিত করে না।
#Cleanup সহ fixture
@pytest.fixture
def temp_store(tmp_path): # tmp_path: pytest এর দেওয়া অস্থায়ী folder
store = JsonStore(tmp_path / "data.json")
yield store # test এখানে চলে
store.close() # test শেষে পরিষ্কার#Mock: বাইরের জিনিস নকল করা
যে function SMS পাঠায় বা payment gateway call করে, test চালালে আসল SMS যাবে না, আসল টাকা কাটবে না। তখন নকল জিনিস দাও। OOP অধ্যায়ের dependency injection এখানে কাজে লাগে:
class FakeNotifier:
def __init__(self) -> None:
self.sent: list[tuple[str, str]] = []
def send(self, to: str, message: str) -> None:
self.sent.append((to, message)) # পাঠায় না, শুধু মনে রাখে
def test_order_sends_confirmation():
notifier = FakeNotifier()
service = OrderService(notifier=notifier)
service.place(Order(id=1, customer="01711111111"))
assert notifier.sent == [("01711111111", "আপনার order নেওয়া হয়েছে")]এভাবে নিজের নকল class বানানো সবচেয়ে পরিষ্কার। Dependency inject করা না গেলে unittest.mock বা pytest এর monkeypatch দিয়ে ভেতরের জিনিস বদলানো যায়:
def test_weather_fallback(monkeypatch):
def fake_get(*args, **kwargs):
raise httpx.TimeoutException("timeout")
monkeypatch.setattr(httpx.Client, "get", fake_get)
assert get_temperature_or_default("Dhaka") == 30.0#Async test
uv add --dev pytest-asyncioimport pytest
@pytest.mark.asyncio
async def test_fetch_user():
user = await fetch_user(1)
assert user["id"] == 1FastAPI এর API test আমরা API Testing অধ্যায়ে দেখবো।
#Coverage: কতটা code test হলো
uv add --dev pytest-cov
uv run pytest --cov=src --cov-report=term-missingName Stmts Miss Cover Missing
------------------------------------------------------
src/shop/pricing.py 12 0 100%
src/shop/cart.py 40 6 85% 51-56Missing বলে কোন লাইন কোনো test এ চলেনি। ১০০% coverage লক্ষ্য না। লক্ষ্য হলো গুরুত্বপূর্ণ logic (টাকা, permission, status বদল) পুরোপুরি test করা।
#Code quality tool
#Ruff: format আর lint
uv run ruff format . # code সাজাও
uv run ruff check . # সম্ভাব্য bug আর খারাপ অভ্যাস খোঁজো
uv run ruff check --fix . # যেগুলো নিজে ঠিক করা যায়, ঠিক করোpyproject.toml এ নিয়ম:
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "ASYNC", "S"]
# E,F: মৌলিক ভুল | I: import সাজানো | B: সাধারণ bug (mutable default ইত্যাদি)
# UP: পুরনো syntax আধুনিক করা | SIM: সহজ করা | ASYNC: async এর ভুল | S: security#Pyright: type check
uv run pyright#pre-commit: commit এর আগে সব চেক
uv add --dev pre-commit# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ruff-format
name: ruff format
entry: uv run ruff format
language: system
types: [python]
- id: ruff-check
name: ruff check
entry: uv run ruff check --fix
language: system
types: [python]
- id: pyright
name: pyright
entry: uv run pyright
language: system
types: [python]
pass_filenames: falseuv run pre-commit installএখন প্রতিটা git commit এর আগে এগুলো চলবে। ভুল থাকলে commit হবে না।
#Test আর AI: কাজের পদ্ধতি
#অনুশীলন
#নিজেকে যাচাই করো
Fixture কী, আর কেন ব্যবহার করবে?
Fixture হলো test এর জন্য প্রস্তুতির function (যেমন একটা cart বানানো, অস্থায়ী database)। বারবার একই setup লেখা লাগে না, আর প্রতিটা test নতুন copy পায়, ফলে test গুলো একে অপরকে প্রভাবিত করে না।
SMS পাঠানো function test করার সময় আসল SMS এড়াবে কীভাবে?
Notifier কে বাইরে থেকে দেওয়ার (dependency injection) ব্যবস্থা রাখবে, আর test এ একটা নকল notifier দেবে যা শুধু মনে রাখে কী পাঠানো হতো। তারপর assert করবে ঠিক message ঠিক নম্বরে "পাঠানো" হয়েছে কিনা।
১০০% coverage কি নিশ্চিত করে code এ bug নেই?
না। Coverage শুধু বলে কোন লাইন চলেছে, ঠিক ফল দিয়েছে কিনা না। দুর্বল assert দিয়েও ১০০% coverage হয়। আর অনেক bug লুকিয়ে থাকে এমন input এ যা কেউ test করেনি।
`parametrize` এর সুবিধা কী?
একই test logic অনেকগুলো input/expected জোড়া দিয়ে চালানো যায়, প্রতিটার জন্য আলাদা function লিখতে হয় না। আর কোনটা ব্যর্থ হলো সেটা আলাদা করে দেখা যায়।
চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।