3-qism · Professional Python·16-bob
Testlash, debugging va kod sifati
pytest bilan testlar: assert, fixture'lar, parametrize, tmp_path, monkeypatch, mock; LLM chaqiruvlarini test qilish; coverage; ruff bilan lint va formatlash; pre-commit; debugger va breakpoint().
"Kod ishlaydi" — bu bitta holatda, bitta kompyuterda, bugun ishlaydi degani. Test — kod ertaga ham, o'zgarishlardan keyin ham, hamkasbingiz kompyuterida ham ishlashining kafolati. AI muhandisligida bu ikki barobar muhim: ma'lumotlarni tozalash funksiyasidagi kichik xato butun datasetni buzadi, va buni model yomon o'qitilgandan keyingina sezasiz — haftalar o'tib.
pytest bilan testlar yozish: assert, fixture'lar, parametrize va mocking amaliy loyihada.
1. Nega test yoziladi#
- Regressiyadan himoya: bitta joyni tuzatib, boshqasini buzmaslik.
- Qo'rqmasdan o'zgartirish (refactoring): testlar o'tsa — xatti-harakat saqlangan.
- Hujjat: test funksiya qanday ishlatilishini va nima kutishini ko'rsatadi.
- Dizaynni yaxshilaydi: test yozish qiyin bo'lsa — funksiya juda ko'p ish qilmoqda yoki juda ko'p narsaga bog'liq.
2. pytest asoslari#
uv add --dev pytestLoyiha tuzilmasi:
myproject/
├── textutils.py
├── classify.py
└── tests/
├── conftest.py # umumiy fixture'lar
├── test_textutils.py
└── test_classify.pypytest test_*.py fayllaridagi test_ bilan boshlanadigan funksiyalarni
avtomatik topadi. Tekshirilayotgan kod:
# textutils.py
import json
import re
from pathlib import Path
_APOSTROPHES = str.maketrans({"ʻ": "'", "‘": "'", "’": "'", "`": "'", "ʼ": "'"})
def clean_text(text: str) -> str:
text = text.translate(_APOSTROPHES)
return re.sub(r"\s+", " ", text).strip()
def chunk_text(text: str, size: int, overlap: int = 0) -> list[str]:
if size <= 0:
raise ValueError("size musbat bo'lishi kerak")
if not 0 <= overlap < size:
raise ValueError("overlap 0 va size orasida bo'lishi kerak")
if not text:
return []
step = size - overlap
return [text[i:i + size] for i in range(0, max(len(text) - overlap, 1), step)]
def save_jsonl(path: Path, records: list[dict]) -> int:
with open(path, "w", encoding="utf-8") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
return len(records)Birinchi testlar:
# tests/test_textutils.py
from textutils import clean_text
def test_clean_text_normalizes_apostrophes():
assert clean_text("Oʻzbekiston") == "O'zbekiston"
def test_clean_text_collapses_whitespace():
assert clean_text(" a \n\t b ") == "a b"uv run pytest -q.. [100%] 2 passed in 0.01s
Oddiy assert — pytest uni "qayta yozadi" va xato bo'lsa batafsil
ko'rsatadi:
def test_clean_text_collapses_whitespace():
> assert clean_text(" a \n\t b ") == "a b"
E AssertionError: assert 'a b' == 'a b'
E - a b
E ? -
E + a bFoydali buyruqlar#
| Buyruq | Nima qiladi |
|---|---|
pytest | Hamma testlar |
pytest -q / -v | Qisqa / batafsil chiqish |
pytest tests/test_textutils.py | Bitta fayl |
pytest tests/test_x.py::test_name | Bitta test |
pytest -k chunk | Nomida "chunk" bor testlar |
pytest -x | Birinchi xatoda to'xtash |
pytest --lf | Faqat oxirgi marta yiqilganlar |
pytest -s | print chiqishini ko'rsatish |
pytest --pdb | Xatoda debugger'ga kirish |
Yaxshi test tuzilishi: Arrange — Act — Assert#
def test_save_jsonl_roundtrip(tmp_path):
# Arrange — tayyorlash
path = tmp_path / "out.jsonl"
records = [{"text": "salom"}, {"text": "dunyo"}]
# Act — bitta amal
n = save_jsonl(path, records)
# Assert — natijani tekshirish
assert n == 2
lines = path.read_text(encoding="utf-8").splitlines()
assert [json.loads(line) for line in lines] == recordsTest nomi nima tekshirilayotganini aytishi kerak:
test_chunk_text_rejects_overlap_equal_to_size — test_chunk2 emas.
Yiqilgan test nomini o'qib, muammoni tushunish mumkin bo'lsin.
3. Istisnolarni tekshirish#
import pytest
def test_chunk_text_rejects_zero_size():
with pytest.raises(ValueError, match="musbat"):
chunk_text("abc", size=0)match — xato xabarida shu matn (regex) borligini tekshiradi. Faqat
pytest.raises(ValueError) yetarli emas: boshqa sabab bilan chiqqan
ValueError ham testni "o'tkazib" yuboradi.
Float'lar uchun — pytest.approx (2-bobdagi 0.1 + 0.2 muammosi):
assert 0.1 + 0.2 == pytest.approx(0.3)
assert compute_metrics(...)["f1"] == pytest.approx(0.8571, abs=1e-4)4. parametrize — bitta test, ko'p holat#
@pytest.mark.parametrize(
("text", "size", "overlap", "expected"),
[
("abcdefghij", 4, 0, ["abcd", "efgh", "ij"]),
("abcdefghij", 4, 1, ["abcd", "defg", "ghij"]),
("abc", 10, 0, ["abc"]),
("", 4, 0, []),
],
)
def test_chunk_text(text, size, overlap, expected):
assert chunk_text(text, size, overlap) == expected
@pytest.mark.parametrize(("size", "overlap"), [(0, 0), (4, 4), (4, -1)])
def test_chunk_text_rejects_bad_params(size, overlap):
with pytest.raises(ValueError, match="bo'lishi kerak"):
chunk_text("abc", size, overlap)Har bir holat alohida test sifatida ishlaydi va alohida hisobot beradi. Chegaraviy holatlarni (edge cases) albatta qo'shing: bo'sh kirish, bitta element, maksimal qiymat, noto'g'ri qiymat, Unicode. Xatolarning ko'pi aynan shu yerda yashaydi.
5. Fixture'lar — tayyorlov kodini qayta ishlatish#
Fixture — test uchun kerakli narsani tayyorlaydigan funksiya. Test uni parametr nomi orqali "so'raydi":
# tests/conftest.py — bu fayldagi fixture'lar hamma testlarga ko'rinadi
import pytest
@pytest.fixture
def sample_records():
return [
{"text": "Zo'r mahsulot", "label": "positive"},
{"text": "Yomon xizmat", "label": "negative"},
]
@pytest.fixture
def jsonl_file(tmp_path, sample_records): # fixture boshqa fixture'larni ishlatishi mumkin
path = tmp_path / "data.jsonl"
save_jsonl(path, sample_records)
return pathdef test_read_back(jsonl_file, sample_records):
assert read_jsonl(jsonl_file) == sample_recordsyield bilan tozalash#
@pytest.fixture
def db():
conn = connect(":memory:")
yield conn # ← test shu yerda ishlaydi
conn.close() # test tugagach (xato bo'lsa ham) — tozalash14-bobdagi @contextmanager bilan bir xil g'oya.
pytest'ning tayyor fixture'lari#
| Fixture | Nima beradi |
|---|---|
tmp_path | Har bir test uchun yangi vaqtinchalik papka (Path) |
monkeypatch | Muhit o'zgaruvchilari, atributlar, lug'atlarni vaqtincha o'zgartirish |
capsys | print chiqishini ushlash |
caplog | Log xabarlarini ushlash |
def test_reads_api_key_from_env(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
monkeypatch.delenv("DEBUG", raising=False)
settings = load_settings()
assert settings.api_key == "test-key"
# test tugagach muhit avvalgi holatiga qaytadi
def test_warns_on_bad_row(caplog):
process_lines(['{"text": "ok"}', "buzilgan"])
assert "o'tkazib yuborildi" in caplog.text6. Tashqi bog'liqliklarni ajratish: fake va mock#
Unit testlar tez, deterministik va tarmoqsiz bo'lishi kerak. Haqiqiy LLM API'ni chaqiradigan test: sekin (soniyalar), qimmat (pul), beqaror (javob har safar boshqacha), internetsiz ishlamaydi. Yechim — tashqi qismni almashtirish.
Eng yaxshi yo'l: dependency injection + fake#
Kodni boshidanoq bog'liqlikni parametr sifatida qabul qiladigan
qilib yozing (13 va 15-boblardagi Protocol):
# classify.py
from typing import Protocol
class LLM(Protocol):
def complete(self, prompt: str) -> str: ...
LABELS = {"positive", "negative", "neutral"}
def classify(llm: LLM, text: str) -> str:
answer = llm.complete(f"Sentiment (positive/negative/neutral): {text}").strip().lower()
return answer if answer in LABELS else "neutral"Testda — oddiy soxta (fake) klass:
# tests/conftest.py
import pytest
class FakeLLM:
def __init__(self, reply: str) -> None:
self.reply = reply
self.prompts: list[str] = [] # nima yuborilganini ham tekshirish uchun
def complete(self, prompt: str) -> str:
self.prompts.append(prompt)
return self.reply
@pytest.fixture
def fake_llm():
return FakeLLM # fabrika: testning o'zi javobni tanlaydi# tests/test_classify.py
import pytest
from classify import classify
@pytest.mark.parametrize(("reply", "expected"), [
("positive", "positive"),
(" NEGATIVE\n", "negative"), # bo'sh joy va katta harf
("Menimcha bu ijobiy", "neutral"), # model formatga rioya qilmadi
])
def test_classify_parses_reply(fake_llm, reply, expected):
assert classify(fake_llm(reply), "Zo'r mahsulot") == expected
def test_classify_sends_text_in_prompt(fake_llm):
llm = fake_llm("positive")
classify(llm, "Zo'r mahsulot")
assert "Zo'r mahsulot" in llm.prompts[0]E'tibor bering, nima test qilinyapti: sizning kodingiz — prompt to'g'ri yig'ilishi va model javobi to'g'ri qayta ishlanishi (shu jumladan, model "noto'g'ri" javob bergan holat). Modelning o'zi "aqlli"ligi — bu unit testning ishi emas (pastda "eval" haqida).
unittest.mock — mavjud kodni "yamash"#
Kod bog'liqlikni ichida yaratsa (dependency injection yo'q), patch
ishlatiladi:
# weather.py
import httpx
def get_temperature(city: str) -> float:
resp = httpx.get("https://api.example.com/weather", params={"city": city}, timeout=10)
resp.raise_for_status()
return resp.json()["temp_c"]# tests/test_weather.py
from unittest.mock import Mock, patch
import weather
def test_get_temperature_parses_response():
fake_resp = Mock()
fake_resp.json.return_value = {"temp_c": 18.5}
with patch("weather.httpx.get", return_value=fake_resp) as fake_get:
assert weather.get_temperature("Toshkent") == 18.5
fake_get.assert_called_once()
assert fake_get.call_args.kwargs["params"] == {"city": "Toshkent"}Unit test va eval — farqi#
| Unit test | Eval (baholash) | |
|---|---|---|
| Nimani tekshiradi | Sizning kodingiz mantiqini | Model + prompt sifatini |
| LLM | Fake/mock | Haqiqiy |
| Natija | O'tdi / yiqildi | Ball (masalan, 87% to'g'ri) |
| Tezlik | Millisekundlar | Daqiqalar, pul turadi |
| Qachon ishlaydi | Har commit'da | Prompt/model o'zgarganda, muntazam |
Ikkalasi ham kerak. Eval — kichik "oltin" dataset (to'g'ri javoblari ma'lum 50–500 misol) ustida model natijalarini o'lchash. Uni ham pytest bilan yozish mumkin, lekin alohida belgi bilan:
@pytest.mark.eval # pyproject.toml'da ro'yxatdan o'tkaziladi
def test_sentiment_accuracy(real_llm, golden_set):
correct = sum(classify(real_llm, ex["text"]) == ex["label"] for ex in golden_set)
assert correct / len(golden_set) >= 0.85pytest -m "not eval" — kundalik tez testlar; pytest -m eval — kerak
bo'lganda.
7. Coverage — qaysi kod test qilinmagan#
uv add --dev pytest-cov
uv run pytest --cov=src --cov-report=term-missingName Stmts Miss Cover Missing
--------------------------------------------------
src/textutils.py 18 2 89% 21-22
src/classify.py 7 0 100%Missing — hech bir test bajarmagan qatorlar. Coverage — qaysi kod
umuman sinalmaganini ko'rsatadi, lekin 100% coverage xatosiz kod degani
emas: qatorni bajarish uni to'g'ri tekshirish degani emas. Maqsad — muhim
mantiqning to'liq qoplanishi, raqam ortidan quvish emas.
8. Debugging: printdan keyingi qadam#
breakpoint()#
def compute_loss(preds, labels):
diffs = [p - l for p, l in zip(preds, labels)]
breakpoint() # dastur shu yerda to'xtaydi va interaktiv debugger ochiladi
return sum(d * d for d in diffs) / len(diffs)pdb buyruqlari:
| Buyruq | Ma'nosi |
|---|---|
p diffs | Qiymatni chiqarish |
pp obj | Chiroyli chiqarish |
n | Keyingi qator (funksiyaga kirmasdan) |
s | Keyingi qator (funksiyaga kirib) |
c | Davom ettirish (keyingi breakpoint'gacha) |
l / ll | Kodni ko'rsatish |
w | Chaqiruvlar steki (qayerdaman) |
u / d | Stekda yuqoriga/pastga |
q | Chiqish |
pytest --pdb — test yiqilgan joyda avtomatik debugger ochiladi.
python -m pdb -c continue script.py — istalgan xatoda (post-mortem).
VS Code debugger#
Qator raqami chap tomoniga bosib breakpoint qo'ying, F5 — "Python Debugger:
Current File". O'zgaruvchilar paneli, watch ifodalari, shartli
breakpoint'lar (i == 500 bo'lganda to'xta) — katta loop'larda juda
foydali. Jupyter'da ham ishlaydi.
Muammoni kichraytiring#
Eng samarali debugging texnikasi asboblar emas, usul:
Takrorlang
Xatoni ishonchli takrorlaydigan eng kichik misolni toping (seed'ni qotiring!).Bo'ling
Pipeline'ning qaysi bosqichida noto'g'ri bo'lishini toping — har bosqich chiqishini tekshiring.Gipoteza
"X sababdan bo'lyapti" deb taxmin qiling va uni tekshiradigan bitta tajriba o'ylang.Test yozing
Xatoni takrorlaydigan test yozing, keyin tuzating — test endi regressiyadan himoya qiladi.
9. Kod sifati vositalari: Ruff#
Ruff — Rust'da yozilgan juda tez linter va formatter. U eski vositalar to'plamini (flake8, isort, black, pyupgrade va o'nlab plaginlar) almashtiradi.
uv add --dev ruff
uv run ruff check . # muammolarni topish
uv run ruff check . --fix # avtomatik tuzatiladiganlarini tuzatish
uv run ruff format . # formatlash (black uslubida)# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = [
"E", "F", # pycodestyle, pyflakes — asosiy xatolar (ishlatilmagan import, noma'lum nom)
"I", # importlarni tartiblash
"B", # bugbear — mutable default, except: pass va boshqa tuzoqlar
"UP", # pyupgrade — eskirgan sintaksisni yangilash
"SIM", # soddalashtirish takliflari
"RUF", # ruff'ning o'z qoidalari
]Ruff bu kursdagi ko'p tuzoqlarni avtomatik topadi:
| Qoida | Nima topadi | Bob |
|---|---|---|
B006 | Mutable default argument | 8 |
B008 | Default'da funksiya chaqiruvi | 8 |
E711/E712 | == None, == True | 2, 4 |
F401 | Ishlatilmagan import | 9 |
B905 | zip() strict=siz | 4 |
E722 | Yalang'och except: | 10 |
A001 | Built-in nomni yopish (list = ...) | 8 |
G004 | Logging'da f-string | 10 |
10. pre-commit — har commit'dan oldin avtomatik tekshiruv#
uv add --dev pre-commit# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files # tasodifan 2 GB datasetni commit qilishdan himoya
- id: detect-private-key
- id: end-of-file-fixeruv run pre-commit install # bir marta — endi har `git commit` dan oldin ishlaydiCI'da (GitHub Actions) ham xuddi shu: ruff check, ruff format --check,
mypy, pytest. Shunda "mening kompyuterimda o'tgan edi" muammosi yo'qoladi.
Nima noto'g'ri ketishi mumkin#
| Xato | Oqibat | Tuzatish |
|---|---|---|
| Testda haqiqiy API chaqiruvi | Sekin, qimmat, beqaror | Fake/mock; haqiqiy chaqiruv — alohida eval |
| Testlar bir-biriga bog'liq (tartib muhim) | Alohida ishga tushirganda yiqiladi | Har test o'z ma'lumotini fixture'dan oladi |
pytest.raises(Exception) | Boshqa xato ham "o'tadi" | Aniq tip + match= |
Float'ni == bilan | Tasodifiy yiqilish | pytest.approx |
| Tasodifiylik seed'siz | "Flaky" testlar | random.Random(42), np.random.default_rng(42) |
| Haqiqiy fayl tizimiga yozish | Testlar iz qoldiradi, to'qnashadi | tmp_path |
patch noto'g'ri yo'l bilan | Mock ishlamaydi, haqiqiy chaqiruv ketadi | Ishlatiladigan joyni yamang |
| Faqat "baxtli yo'l" testlari | Chegaraviy holatlardagi xatolar | Bo'sh, bitta, katta, noto'g'ri, Unicode |
| Coverage'ni maqsad qilish | Ma'nosiz testlar | Muhim mantiqni sifatli qoplash |
1. Oldingi funksiyalarga testlar
mlutils paketingizdagi (9-bob) funksiyalarga to'liq test to'plami
yozing: normalize, batched, train_test_split, safe_float.
train_test_split uchun maxsus tekshiring: asl ro'yxat o'zgarmaydi, ikki
qism kesishmaydi, birlashmasi aslga teng, bir xil seed — bir xil natija.
Maslahatni ko'rsatish
Har biri uchun: oddiy holat, chegaraviy holat (bo'sh, bitta), xato holati (pytest.raises). parametrize bilan ixcham.
2. Fake LLM bilan retry'ni test qilish
14-bobdagi @retry dekoratorini test qiling: (a) 2 marta xato, 3-chisida
muvaffaqiyat → natija qaytadi va 3 marta chaqirilgan; (b) har doim xato →
oxirgi xato ko'tariladi; (c) ro'yxatda yo'q xato → darhol ko'tariladi,
qayta urinishsiz. Testlar tez bo'lishi uchun time.sleepni
monkeypatch.setattr bilan o'chiring.
Maslahatni ko'rsatish
FakeLLM'ga 'xatolar ro'yxati' bering: har chaqiruvda ro'yxatdan navbatdagisini ko'tarsin; time.sleep ni monkeypatch bilan o'chiring.
3. Bug'ni test bilan tuzating
7-bobdagi chunk_text mashqidagi versiyangiz bilan bu bobdagi versiyani
solishtiring: chunk_text("abcdefghij", 4, 1) ikkalasida bir xil natija
beradimi? Farqni ko'rsatadigan test yozing va qaysi xatti-harakat to'g'ri
ekanini asoslab, hujjatga yozing.
Maslahatni ko'rsatish
Avval xatoni ko'rsatadigan test yozing (u yiqilishi kerak), keyin kodni tuzating.
4. Sifat konveyeri
Loyihangizga Ruff (yuqoridagi qoidalar bilan), mypy va pre-commit'ni
ulang. Barcha ogohlantirishlarni tuzating. Keyin GitHub Actions
workflow'ini yozing: har push'da uv sync, ruff check, ruff format --check,
mypy, pytest -m "not eval".
- 01pytest: test_ funksiyalar, oddiy assert, -k, -x, --lf, --pdb.
- 02Arrange–Act–Assert va tushunarli test nomlari.
- 03pytest.raises(..., match=...), pytest.approx, parametrize — chegaraviy holatlar bilan.
- 04Fixture'lar (conftest.py), yield bilan tozalash; tmp_path, monkeypatch, caplog.
- 05Tashqi bog'liqliklar: dependency injection + fake eng yaxshi; patch — ishlatiladigan joyda.
- 06Unit test kodingizni, eval esa model+prompt sifatini o'lchaydi — ikkalasi ham kerak.
- 07breakpoint() va pdb; muammoni takrorlang → bo'ling → gipoteza → test.
- 08Ruff (lint + format), mypy, pre-commit va CI — sifat konveyeri.
Mustahkamlash
Test va amaliy masalalar
Avval testni ishlang — har javobdan keyin izoh chiqadi. Keyin masalalarni shu yerning o'zida yechib, brauzerdagi haqiqiy Python bilan avtomatik tekshiring. Natijalaringiz kurs progressi sahifasida jamlanadi.
Bilimni tekshirish
Bob bo'yicha test
12 ta savol · har javobdan keyin to'g'ri yoki xatoligi va izoh ko'rsatiladi · oxirida natija foizda.
Amaliy masalalar
Kodni yozing va «Tekshirish»ni bosing (yoki Ctrl/⌘ + Enter). Kodingiz shu brauzerda saqlanib turadi.
01O'zingiz test yozing
Quyidagi slugify(title) funksiyasi berilgan (u to'g'ri ishlaydi). Uni tekshiradigan test_slugify() funksiyasini yozing — kamida 4 ta assert: oddiy holat, bo'sh joylar, katta harf, belgilar olib tashlanishi. Tekshiruvchi sizning testingizni to'g'ri va ataylab buzilgan versiyada ishga tushiradi: to'g'risida o'tishi, buzilganida yiqilishi kerak.
02Fake LLM bilan tasniflash
classify(llm, text) — llm.complete(prompt) ni chaqirib, javobni tozalab (strip().lower()), positive/negative/neutral dan biri bo'lsa qaytarsin, aks holda "neutral". Prompt ichida text bo'lsin. Testlar o'zining FakeLLM klassidan foydalanadi.
03Parametrize'ga tayyor funksiya
run_cases(fn, cases) — mini test-runner: cases — [(args_tuple, kutilgan), ...]. Har holat uchun fn(*args) ni chaqirib, (o'tganlar_soni, [yiqilgan_holat_indekslari]) qaytarsin. Funksiya xato ko'tarsa — yiqilgan hisoblansin.