aidoc.uz

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.

▶ Video dars· Tech With Tim· 33:09· ingliz tilida
Please Learn How To Write Tests in Python… • Pytest Tutorial

pytest bilan testlar yozish: assert, fixture'lar, parametrize va mocking amaliy loyihada.

YouTube'da ochish ↗

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 pytest

Loyiha tuzilmasi:

myproject/
├── textutils.py
├── classify.py
└── tests/
    ├── conftest.py          # umumiy fixture'lar
    ├── test_textutils.py
    └── test_classify.py

pytest 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"
terminal
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 b

Foydali buyruqlar#

BuyruqNima qiladi
pytestHamma testlar
pytest -q / -vQisqa / batafsil chiqish
pytest tests/test_textutils.pyBitta fayl
pytest tests/test_x.py::test_nameBitta test
pytest -k chunkNomida "chunk" bor testlar
pytest -xBirinchi xatoda to'xtash
pytest --lfFaqat oxirgi marta yiqilganlar
pytest -sprint chiqishini ko'rsatish
pytest --pdbXatoda 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] == records

Test 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 path
def test_read_back(jsonl_file, sample_records):
    assert read_jsonl(jsonl_file) == sample_records

yield bilan tozalash#

@pytest.fixture
def db():
    conn = connect(":memory:")
    yield conn              # ← test shu yerda ishlaydi
    conn.close()            # test tugagach (xato bo'lsa ham) — tozalash

14-bobdagi @contextmanager bilan bir xil g'oya.

pytest'ning tayyor fixture'lari#

FixtureNima beradi
tmp_pathHar bir test uchun yangi vaqtinchalik papka (Path)
monkeypatchMuhit o'zgaruvchilari, atributlar, lug'atlarni vaqtincha o'zgartirish
capsysprint chiqishini ushlash
caplogLog 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.text

6. 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 testEval (baholash)
Nimani tekshiradiSizning kodingiz mantiqiniModel + prompt sifatini
LLMFake/mockHaqiqiy
NatijaO'tdi / yiqildiBall (masalan, 87% to'g'ri)
TezlikMillisekundlarDaqiqalar, pul turadi
Qachon ishlaydiHar commit'daPrompt/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.85

pytest -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-missing
Name                 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:

BuyruqMa'nosi
p diffsQiymatni chiqarish
pp objChiroyli chiqarish
nKeyingi qator (funksiyaga kirmasdan)
sKeyingi qator (funksiyaga kirib)
cDavom ettirish (keyingi breakpoint'gacha)
l / llKodni ko'rsatish
wChaqiruvlar steki (qayerdaman)
u / dStekda yuqoriga/pastga
qChiqish

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:

  1. Takrorlang

    Xatoni ishonchli takrorlaydigan eng kichik misolni toping (seed'ni qotiring!).
  2. Bo'ling

    Pipeline'ning qaysi bosqichida noto'g'ri bo'lishini toping — har bosqich chiqishini tekshiring.
  3. Gipoteza

    "X sababdan bo'lyapti" deb taxmin qiling va uni tekshiradigan bitta tajriba o'ylang.
  4. 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:

QoidaNima topadiBob
B006Mutable default argument8
B008Default'da funksiya chaqiruvi8
E711/E712== None, == True2, 4
F401Ishlatilmagan import9
B905zip() strict=siz4
E722Yalang'och except:10
A001Built-in nomni yopish (list = ...)8
G004Logging'da f-string10

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-fixer
uv run pre-commit install       # bir marta — endi har `git commit` dan oldin ishlaydi

CI'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#

XatoOqibatTuzatish
Testda haqiqiy API chaqiruviSekin, qimmat, beqarorFake/mock; haqiqiy chaqiruv — alohida eval
Testlar bir-biriga bog'liq (tartib muhim)Alohida ishga tushirganda yiqiladiHar test o'z ma'lumotini fixture'dan oladi
pytest.raises(Exception)Boshqa xato ham "o'tadi"Aniq tip + match=
Float'ni == bilanTasodifiy yiqilishpytest.approx
Tasodifiylik seed'siz"Flaky" testlarrandom.Random(42), np.random.default_rng(42)
Haqiqiy fayl tizimiga yozishTestlar iz qoldiradi, to'qnashaditmp_path
patch noto'g'ri yo'l bilanMock ishlamaydi, haqiqiy chaqiruv ketadiIshlatiladigan joyni yamang
Faqat "baxtli yo'l" testlariChegaraviy holatlardagi xatolarBo'sh, bitta, katta, noto'g'ri, Unicode
Coverage'ni maqsad qilishMa'nosiz testlarMuhim mantiqni sifatli qoplash
Amaliy topshiriq

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.

Amaliy topshiriq

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.

Amaliy topshiriq

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.

Amaliy topshiriq

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".

Yodda saqlang
  • 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.

Test: topshirilmaganMasalalar: 0/3

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.