13 β Dasturni test qilish (pytest)¶
Kod yozdik β lekin u to'g'ri ishlayaptimi? Har safar qo'lda tekshirish (dasturni ishga tushirib, natijani ko'rib) zerikarli va ishonchsiz. Testlar β kodning to'g'ri ishlashini avtomatik tekshiradigan kod. Python'da buning eng mashhur vositasi β pytest. Bu modulda ishonchli test yozishni o'rganasan.
Bu modulda: nega test kerak,
pytestasoslari (assert, test funksiyalari), fixture'lar, parametrlash, va xatolarni test qilish.
13.1 Nega test yozamiz?¶
Tasavvur qil: katta dasturda bir joyni o'zgartirding. Boshqa joy buzilmaganini qanday bilasan? Qo'lda hammasini sinab ko'rish β uzoq va xatoga yo'l qo'yiladi. Testlar bir buyruq bilan hamma narsani tekshiradi.
pytest ni o'rnatish (terminalda):
13.2 Birinchi test¶
Test β assert ishlatadigan oddiy funksiya. assert shartni tekshiradi: agar True bo'lsa hech narsa, False bo'lsa xato (test "yiqiladi"):
# matematika.py
def qosh(a: int, b: int) -> int: # 3.13: tip ko'rsatmalari bilan β niyat aniq
return a + b
# test_matematika.py β fayl nomi test_ bilan boshlanishi SHART
from matematika import qosh
def test_qosh(): # funksiya nomi ham test_ bilan
assert qosh(2, 3) == 5 # "qosh(2,3) natijasi 5 ga teng bo'lishi kerak"
def test_qosh_manfiy():
assert qosh(-1, -1) == -2
Testlarni ishga tushirish (terminalda, fayl turgan papkada):
pytest # barcha test_*.py fayllarni topib ishga tushiradi
pytest -v # batafsil (har test nomi ko'rinadi)
Hammasi to'g'ri bo'lsa, yashil PASSED chiqadi. Xato bo'lsa, qaysi test va nima sababdan yiqilganini ko'rsatadi.
Discovery qoidasi:
pytesttest_bilan boshlanadigan fayllardagitest_bilan boshlanadigan funksiyalarni avtomatik topadi. Hech narsa ro'yxatga olish kerak emas.
Har bir test odatda bir xil uch bosqichdan iborat bo'ladi: tayyorla, ishlat, tekshir.
13.3 assert β turli tekshiruvlar¶
def test_misollar():
assert 2 + 2 == 4
assert "salom" in "salom dunyo" # ichida bormi
assert len([1, 2, 3]) == 3
assert [1, 2] == [1, 2] # ro'yxatlar teng
natija = None
assert natija is None
assert 5 > 3
pytest aqlli β test yiqilsa, nima kutilgani va nima chiqqanini ko'rsatadi:
13.4 Fixture β test uchun tayyor ma'lumot¶
Ko'p testga bir xil tayyorgarlik kerak bo'lsa, fixture ishlatasan. Fixture β testga argument sifatida uzatiladigan tayyor ma'lumot:
import pytest
@pytest.fixture
def namuna_talaba():
return {"ism": "Aziz", "yosh": 20}
def test_ism(namuna_talaba): # fixture nomini argument qilib ol
assert namuna_talaba["ism"] == "Aziz"
def test_yosh(namuna_talaba):
assert namuna_talaba["yosh"] >= 18
pytest namuna_talaba ni avtomatik chaqirib, natijasini testga uzatadi. Bu takror tayyorgarlikni kamaytiradi.
Quyidagi diagramma bitta fixture qanday qilib bir nechta testga tayyor ma'lumot berishini ko'rsatadi.
13.5 parametrize β bitta test, ko'p holat¶
Bir testni turli kirishlar bilan tekshirish uchun:
import pytest
@pytest.mark.parametrize("kirish, kutilgan", [
(2, 4),
(3, 9),
(4, 16),
(0, 0),
])
def test_kvadrat(kirish, kutilgan):
assert kirish ** 2 == kutilgan
# Bu 4 ta alohida test sifatida ishlaydi
Bu juda foydali: bir funksiyani 10 ta holat bilan sinashing kerak bo'lsa, 10 ta alohida test yozish o'rniga bitta
parametrizejadval yozasan.
Diagrammada jadvaldagi har bir qator alohida test sifatida ishlashini ko'rishing mumkin.
13.6 Xatolarni test qilish¶
Funksiya kerakli vaqtda xato chaqirishini ham test qilish kerak (7-modulni esla):
import pytest
def bol(a: float, b: float) -> float:
if b == 0:
raise ValueError("nolga bo'lish mumkin emas")
return a / b
def test_normal():
assert bol(10, 2) == 5
def test_nolga_bolish():
with pytest.raises(ValueError): # "bu yerda ValueError chiqishi KERAK"
bol(10, 0)
pytest.raises ichidagi kod kutilgan xatoni chaqirmasa, test yiqiladi. Ya'ni "xato chiqishi shart" ekanini tekshiradi.
13.7 TDD β avval test, keyin kod¶
Ilg'or amaliyot TDD (Test-Driven Development): avval test yozasan (u yiqiladi, chunki kod yo'q), keyin testni o'tkazadigan kod yozasan. Bu kodni aniq talablar asosida yozishga majbur qiladi.
# 1. Avval test (yiqiladi):
def test_juft():
assert juftmi(4) is True
assert juftmi(3) is False
# 2. Keyin testni o'tkazadigan kod:
def juftmi(n: int) -> bool:
return n % 2 == 0
Hozircha TDD'ni qat'iy qo'llash shart emas. Lekin "kodimni qanday test qilaman?" deb o'ylash β kodni yaxshiroq yozishga yordam beradi.
13.8 unittest.mock β soxta obyektlar (Mock, MagicMock, patch)¶
Tasavvur qil: funksiyang internetdan ob-havo oladi yoki ma'lumotlar bazasiga yozadi. Bunday kodni test qilish qiyin β har testda haqiqiy tarmoq yoki bazaga murojaat qilsang, test sekin, ishonchsiz va internetga bog'liq bo'ladi. Yechim β soxta obyekt (mock): haqiqiy obyektga o'xshab tutadigan, lekin biz nazorat qiladigan "qalbaki" obyekt. Python'da buning vositasi standart kutubxonadagi unittest.mock.
Mock asoslari¶
Mock() β har qanday metod va atributni "qabul qiladigan" sehrli obyekt. Unga oldindan "shuni qaytar" deb aytib qo'yasan:
from unittest.mock import Mock
def test_mock_asoslari():
soxta = Mock()
soxta.olish.return_value = {"ism": "Aziz", "yosh": 20} # "olish() chaqirilsa, shuni qaytar"
natija = soxta.olish(5)
assert natija == {"ism": "Aziz", "yosh": 20}
soxta.olish.assert_called_once() # roppa-rosa bir marta chaqirilganmi
soxta.olish.assert_called_with(5) # 5 argumenti bilan chaqirilganmi
assert soxta.olish.call_count == 1 # necha marta chaqirildi
# PASSED β mock chaqiruvlarni "eslab qoladi"
Mock'ning kuchi: u nafaqat qiymat qaytaradi, balki qanday chaqirilganini ham yozib boradi. assert_called_with orqali funksiyang to'g'ri argument bilan chaqirilganini tekshirasan.
MagicMock β sehrli metodlar bilan¶
MagicMock β Mock'ning kuchliroq versiyasi: len(), [] (indekslash), in kabi maxsus (dunder) operatorlarni ham qo'llab-quvvatlaydi:
from unittest.mock import MagicMock
def test_magicmock():
soxta = MagicMock()
soxta.__len__.return_value = 3
assert len(soxta) == 3 # len() ishlaydi
soxta[0] = "salom" # indeks bilan yozish ham ishlaydi
qiymat = soxta["kalit"] # o'qish β yana MagicMock qaytaradi
assert isinstance(qiymat, MagicMock)
# PASSED
Qachon qaysi biri?
patchodatda avtomatikMagicMockberadi. Oddiy holatdaMock,len/[]kerak bo'lsaMagicMockishlat.
patch β haqiqiy narsani vaqtincha almashtirish¶
Eng kuchli qurol β patch. U test davomida haqiqiy funksiya/obyektni soxtasi bilan almashtiradi, test tugagach o'z joyiga qaytaradi. Misol: tarmoqqa chiqadigan funksiyani tarmoqsiz test qilamiz:
import urllib.request
def ob_havo(shahar: str) -> str:
javob = urllib.request.urlopen(f"https://api.example.com/{shahar}")
return javob.read().decode()
from unittest.mock import patch
def test_ob_havo_patch():
with patch("urllib.request.urlopen") as soxta_url:
# urlopen(...).read() nima qaytarishini biz belgilaymiz:
soxta_url.return_value.read.return_value = b"Quyoshli, +25"
natija = ob_havo("Toshkent")
assert natija == "Quyoshli, +25"
soxta_url.assert_called_once_with("https://api.example.com/Toshkent")
# PASSED β internet umuman ishlatilmadi!
patch ni dekorator sifatida ham yozish mumkin (soxta obyekt argument bo'lib keladi):
@patch("urllib.request.urlopen")
def test_ob_havo_dekorator(soxta_url):
soxta_url.return_value.read.return_value = b"Bulutli, +18"
assert ob_havo("Samarqand") == "Bulutli, +18"
# PASSED
Muhim qoida:
patch("urllib.request.urlopen")da yo'l β funksiya ishlatilgan joy, e'lon qilingan joy emas. Ya'nimymoduleichidafrom x import fqilgan bo'lsang,patch("mymodule.f")deb yozasan,patch("x.f")emas. Bu eng ko'p uchraydigan xato.
side_effect β ketma-ket qiymat yoki xato¶
return_value har safar bir xil qiymat qaytaradi. side_effect esa har chaqiruvda boshqa qiymat berishi yoki xato chaqirishi mumkin:
def test_side_effect_qiymatlar():
soxta = Mock()
soxta.keyingi.side_effect = [1, 2, 3] # har chaqiruvda navbatdagisi
assert soxta.keyingi() == 1
assert soxta.keyingi() == 2
assert soxta.keyingi() == 3
def test_side_effect_xato():
soxta = Mock()
soxta.ishla.side_effect = ValueError("uzilish") # chaqirilsa β xato
with pytest.raises(ValueError):
soxta.ishla()
# Tarmoq uzilishi, timeout kabi xato holatlarini shunday taqlid qilamiz
Quyidagi diagramma patch test davomida haqiqiy funksiyani soxtasiga almashtirib, keyin qaytarishini ko'rsatadi.
13.9 monkeypatch β pytest uslubidagi almashtirish¶
patch β unittestdan. pytestning o'zida shu vazifani bajaradigan, qulayroq monkeypatch fixture'i bor. U environ (muhit o'zgaruvchilari), atribut va funksiyalarni vaqtincha almashtiradi β va test tugagach avtomatik tiklaydi (qo'lda with yozish shart emas).
Muhit o'zgaruvchilarini almashtirish¶
import os
def maxfiy_kalit() -> str:
kalit = os.environ.get("API_KEY")
if kalit is None:
raise RuntimeError("API_KEY o'rnatilmagan")
return kalit
def test_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test-123") # vaqtincha o'rnat
assert maxfiy_kalit() == "test-123"
def test_env_yoq(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False) # vaqtincha o'chir
with pytest.raises(RuntimeError):
maxfiy_kalit()
# Ikkala test ham haqiqiy muhitga ta'sir qilmaydi β pytest o'zi tozalaydi
Funksiyani almashtirish β tasodifni "boshqarish"¶
Tasodifiy (random) yoki vaqtga bog'liq kodni test qilish qiyin β natija har safar boshqacha. monkeypatch.setattr bilan tasodifni "qotirib qo'yamiz":
import random
def tanga() -> str:
return "bosh" if random.random() < 0.5 else "yon"
def test_tanga_bosh(monkeypatch):
monkeypatch.setattr(random, "random", lambda: 0.1) # 0.1 < 0.5 β bosh
assert tanga() == "bosh"
def test_tanga_yon(monkeypatch):
monkeypatch.setattr(random, "random", lambda: 0.9) # 0.9 > 0.5 β yon
assert tanga() == "yon"
# Endi tasodif emas β har ikkala tarmoqni ham aniq test qildik
input() ni almashtirish¶
Foydalanuvchidan ma'lumot so'raydigan kodni test qilish uchun input ni almashtiramiz:
def yosh_sora() -> int:
return int(input("Yoshing: "))
def test_yosh_sora(monkeypatch):
monkeypatch.setattr("builtins.input", lambda matn="": "25")
assert yosh_sora() == 25
# Klaviaturadan hech narsa kiritmasdan input()'ni test qildik
patchyokimonkeypatch? Ikkalasi bir xil ishni qiladi.monkeypatchβ pytest fixture'i, qisqaroq vawithsiz.patchβunittestdan, ko'p loyihada uchraydi vaassert_called_withkabi tekshiruvlar bilan birga keladi. Atribut/env almashtirishdamonkeypatch, chaqiruvlarni tekshirishdaMock/patchqulayroq.
13.10 Fixture chuqurroq: conftest.py, scope, yield va bog'liqlik¶
13.4-bo'limda fixture asoslarini ko'rdik. Endi uni professional darajada ishlatishni o'rganamiz.
yield bilan tozalash (teardown)¶
Ko'pincha fixture nimadir ochadi (fayl, ulanish) β testdan keyin uni yopish kerak. Buning uchun return o'rniga yield ishlatamiz: yieldgacha β tayyorgarlik (setup), yielddan keyin β tozalash (teardown):
import pytest
@pytest.fixture
def vaqtinchalik_fayl(tmp_path):
fayl = tmp_path / "malumot.txt"
fayl.write_text("boshlang'ich", encoding="utf-8") # SETUP
yield fayl # testga beriladi
# --- test tugagach bu yer ishlaydi ---
if fayl.exists():
fayl.unlink() # TEARDOWN: tozalash
def test_fayl_oqish(vaqtinchalik_fayl):
assert vaqtinchalik_fayl.read_text(encoding="utf-8") == "boshlang'ich"
# Test o'tdi yoki yiqildimi β teardown baribir ishlaydi
scope β fixture qanchalik "yashaydi"¶
Sukut bo'yicha fixture har test uchun qaytadan yaratiladi (scope="function"). Agar tayyorgarlik qimmat bo'lsa (masalan, bazaga ulanish), uni bir marta yaratib, ko'p testga ulashish mumkin:
@pytest.fixture(scope="module") # butun fayl uchun BIR MARTA
def ulanish():
print("\n>>> Ulanish OCHILDI (module boshida bir marta)")
ulanish_obyekti = {"holat": "ochiq", "sorovlar": 0}
yield ulanish_obyekti
print("\n>>> Ulanish YOPILDI (module oxirida bir marta)")
def test_birinchi_sorov(ulanish):
ulanish["sorovlar"] += 1
assert ulanish["holat"] == "ochiq"
def test_ikkinchi_sorov(ulanish): # AYNI o'sha ulanish obyekti
ulanish["sorovlar"] += 1
assert ulanish["sorovlar"] >= 1
scope qiymatlari: "function" (sukut, har test), "class" (har klass), "module" (har fayl), "session" (butun pytest ishi davomida bir marta).
conftest.py β fixture'larni baham ko'rish¶
Bir nechta test faylida bir xil fixture kerak bo'lsa, uni conftest.py fayliga joylaysan. pytest uni avtomatik topadi β import qilish shart emas:
# conftest.py (test fayllar yonida turadi)
import pytest
@pytest.fixture
def admin_user():
return {"ism": "Admin", "rol": "admin"}
# test_huquq.py β import YO'Q, baribir ishlaydi
def test_admin(admin_user):
assert admin_user["rol"] == "admin"
Fixture bog'liqligi (kompozitsiya)¶
Fixture boshqa fixture'ni argument sifatida olishi mumkin β bu "qatlam-qatlam" tayyorgarlik yasashga imkon beradi:
@pytest.fixture
def bosh_baza():
return {"talabalar": []}
@pytest.fixture
def tola_baza(bosh_baza): # bosh_baza'ni ishlatadi
bosh_baza["talabalar"].append({"ism": "Aziz", "ball": 90})
bosh_baza["talabalar"].append({"ism": "Gul", "ball": 75})
return bosh_baza
def test_bosh(bosh_baza):
assert bosh_baza["talabalar"] == []
def test_tola(tola_baza):
assert len(tola_baza["talabalar"]) == 2
assert tola_baza["talabalar"][0]["ism"] == "Aziz"
13.11 Markerlar: skip, skipif, xfail va tanlab ishga tushirish¶
Marker β testga yopishtiriladigan "yorliq" (@pytest.mark.*). U pytestga test haqida qo'shimcha ma'lumot beradi.
skip va skipif β testni o'tkazib yuborish¶
Ba'zi testlar muayyan sharoitda ishlamaydi (masalan, faqat Windows'da, yoki funksiya hali yozilmagan). Ularni o'tkazib yuborish mumkin:
import sys
import pytest
@pytest.mark.skip(reason="bu funksiya hali yozilmagan")
def test_kelajak():
assert hali_yoq() == 42 # ishlamaydi, lekin pytest yiqilmaydi
@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ kerak")
def test_match_case():
qiymat = 200
match qiymat: # match/case β 3.10+
case 200:
natija = "ok"
case _:
natija = "boshqa"
assert natija == "ok"
skip β har doim o'tkazib yuboradi; skipif β shart True bo'lsa o'tkazib yuboradi.
xfail β "yiqilishi kutilyapti"¶
Ma'lum bug bor, lekin hozir tuzatolmaysan? xfail (expected fail) bilan belgila β test yiqilsa, pytest buni kutilgan deb hisoblaydi (qizil emas):
@pytest.mark.xfail(reason="ma'lum bug, hali tuzatilmagan")
def test_malum_xato():
assert (0.1 + 0.2) == 0.3 # yiqiladi β XFAIL (kutilgan)
Agar bug to'satdan tuzalsa, test "kutilmaganda o'tdi" (XPASS) deb belgilanadi β bu signal: xfailni olib tashlash vaqti keldi.
parametrize chuqurroq β id va xato holatlari¶
13.5-bo'limdagi parametrizeni kuchaytiramiz. pytest.param bilan har holatga o'qiladigan nom (id) berish mumkin β test natijasida chiroyli ko'rinadi:
@pytest.mark.parametrize("kirish, kutilgan", [
pytest.param("radar", True, id="palindrom"),
pytest.param("salom", False, id="oddiy-soz"),
pytest.param("", True, id="bosh-satr"),
])
def test_palindrom(kirish, kutilgan):
assert (kirish == kirish[::-1]) == kutilgan
# Natijada: test_palindrom[palindrom], test_palindrom[oddiy-soz] ...
Bir testga ikkita parametrize qo'ysang β ular dekart ko'paytmasi bo'lib ishlaydi (hamma kombinatsiya):
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_kopaytma(x, y):
assert x * y > 0
# 2 x 2 = 4 ta test: (1,10), (1,20), (2,10), (2,20)
-k va -m β testlarni tanlab ishga tushirish¶
Katta loyihada har safar hamma testni ishlatish shart emas. pytest tanlash imkonini beradi:
pytest -k "palindrom" # nomida "palindrom" bor testlar
pytest -k "qosh or bol" # nomida "qosh" YOKI "bol" bor
pytest -m "sekin" # @pytest.mark.sekin bilan belgilangan
pytest -m "not sekin" # sekin BO'LMAGAN testlar (tez ishlash)
pytest test_fayl.py::test_qosh # aniq bitta test
Maxsus markerni conftest.py da ro'yxatdan o'tkazasan (ogohlantirish chiqmasligi uchun):
# conftest.py
def pytest_configure(config):
config.addinivalue_line("markers", "sekin: sekin ishlaydigan testlar")
@pytest.mark.sekin
def test_ogir_hisob():
natija = sum(i * i for i in range(1_000_000))
assert natija > 0
13.12 pytest.approx β kasr sonlarni to'g'ri taqqoslash¶
Kompyuter kasr (float) sonlarni aniq saqlay olmaydi. Klassik tuzoq:
def test_float_xato():
assert (0.1 + 0.2) == 0.3 # YIQILADI!
# chunki 0.1 + 0.2 = 0.30000000000000004
Yechim β pytest.approx: "taxminan teng" deb tekshiradi:
def test_approx():
assert (0.1 + 0.2) == pytest.approx(0.3) # PASSED
def test_approx_aniqlik():
assert 3.14159 == pytest.approx(3.14, abs=0.01) # 0.01 farq joiz
def test_approx_royxat():
assert [0.1 + 0.2, 1.0] == pytest.approx([0.3, 1.0]) # ro'yxatlar ham
Qoida: float natijalarni HECH QACHON
==bilan taqqoslama. Har doimpytest.approxishlat.abs=β mutlaq farq,rel=β nisbiy farq chegarasi.
13.13 tmp_path, capsys, caplog β pytest o'rnatilgan fixture'lar¶
pytest bir necha foydali fixture'ni o'zi bilan beradi β ularni e'lon qilmasdan ishlataverasan.
tmp_path β vaqtinchalik papka¶
Faylga yozadigan kodni test qilishda haqiqiy fayl yaratish xavfli (eski faylni buzishi mumkin). tmp_path har testga toza, vaqtinchalik papka beradi (pathlib.Path), test tugagach o'zi o'chiriladi:
def faylga_yoz(yol, matn: str) -> int:
yol.write_text(matn, encoding="utf-8")
return len(matn)
def test_fayl_yozish(tmp_path):
fayl = tmp_path / "natija.txt" # vaqtinchalik papkada
uzunlik = faylga_yoz(fayl, "salom dunyo")
assert fayl.exists()
assert uzunlik == 11
assert fayl.read_text(encoding="utf-8") == "salom dunyo"
capsys β print chiqishini ushlash¶
print qiladigan funksiyani test qilish uchun capsys chiqishni ushlaydi:
def salomlash(ism: str) -> None:
print(f"Salom, {ism}!")
print("Xush kelibsiz")
def test_chiqishni_tekshir(capsys):
salomlash("Aziz")
chiqildi = capsys.readouterr() # ushlangan chiqishni o'qib ol
assert "Salom, Aziz!" in chiqildi.out
assert "Xush kelibsiz" in chiqildi.out
caplog β log xabarlarini tekshirish¶
logging ishlatadigan kod to'g'ri ogohlantirish berayotganini tekshirish uchun caplog:
import logging
logger = logging.getLogger(__name__)
def hisobla(x: int) -> int:
if x < 0:
logger.warning("manfiy son: %s", x)
x = abs(x)
return x * 2
def test_loglarni_tekshir(caplog):
with caplog.at_level(logging.WARNING):
natija = hisobla(-5)
assert natija == 10
assert "manfiy son: -5" in caplog.text
13.14 pytest-cov β qamrov (coverage) o'lchash¶
Testlar yozding β lekin ular kodingning qancha qismini tekshiryapti? Coverage (qamrov) β kodning necha foizi testlar davomida ishga tushganini ko'rsatadigan o'lchov. Buning vositasi β pytest-cov:
Ishlatish (mendan β o'lchanadigan modul/papka nomi):
pytest --cov=mendan # qamrov foizi
pytest --cov=mendan --cov-report=term-missing # qaysi QATORLAR qoldi β ko'rsatadi
pytest --cov=mendan --cov-report=html # htmlcov/ papkada chiroyli hisobot
Natija taxminan shunday ko'rinadi:
Name Stmts Miss Cover Missing
-------------------------------------------
mendan.py 20 3 85% 14-16
-------------------------------------------
TOTAL 20 3 85%
Missing ustuni β testlar tegmagan qatorlar (bu yerda 14β16). Aynan ularga test yozsang, qamrov 100%ga yaqinlashadi.
Ogohlantirish: 100% qamrov β kod xatosiz degani EMAS. U faqat "har qator ishga tushdi"ni bildiradi, "har holat to'g'ri" degani emas. Qamrov β foydali ko'rsatkich, lekin sifat o'rnini bosmaydi. Test sifati β yaxshi
assertlarda, raqamlarda emas.
13.15 FastAPI'ni test qilish: TestClient¶
Keyingi (14-) modulda FastAPI bilan veb API yasaysan. Bunday API'ni qanday test qilamiz β serverni qo'lda ishga tushirib, brauzerdan tekshiribmi? Yo'q! FastAPI TestClient beradi: u serverni ishga tushirmasdan, kodda to'g'ridan-to'g'ri so'rov yuboradi va javobni tekshiradi. Bu juda tez va ishonchli.
Avval kichik API (14-modul uslubida):
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Talaba(BaseModel):
ism: str
yosh: int
baza: dict[int, Talaba] = {}
@app.get("/")
def bosh():
return {"xabar": "Salom API"}
@app.get("/talabalar/{id}")
def bittasi(id: int):
if id not in baza:
raise HTTPException(status_code=404, detail="Topilmadi")
return baza[id]
@app.post("/talabalar/{id}")
def qoshish(id: int, talaba: Talaba):
baza[id] = talaba
return {"holat": "qo'shildi", "id": id}
Endi testlar β TestClientni fixture qilib, har testga toza klient beramiz:
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def klient():
baza.clear() # har test toza bazadan boshlasin
return TestClient(app)
def test_bosh_sahifa(klient):
javob = klient.get("/")
assert javob.status_code == 200
assert javob.json() == {"xabar": "Salom API"}
def test_qoshish(klient):
javob = klient.post("/talabalar/1", json={"ism": "Aziz", "yosh": 20})
assert javob.status_code == 200
assert javob.json()["holat"] == "qo'shildi"
def test_oqish(klient):
klient.post("/talabalar/5", json={"ism": "Gul", "yosh": 19})
javob = klient.get("/talabalar/5")
assert javob.status_code == 200
assert javob.json() == {"ism": "Gul", "yosh": 19}
def test_topilmadi(klient):
javob = klient.get("/talabalar/999")
assert javob.status_code == 404
assert javob.json()["detail"] == "Topilmadi"
def test_notogri_malumot(klient):
javob = klient.post("/talabalar/2", json={"ism": "Vali"}) # yosh yo'q
assert javob.status_code == 422 # FastAPI o'zi tekshiradi (Pydantic)
E'tibor ber: test_notogri_malumot da yosh yubormasak, FastAPI avtomatik 422 qaytaradi β bu Pydantic tekshiruvi (14-modulni esla). TestClient haqiqiy HTTP klientga o'xshaydi (.get, .post, .json()), lekin tarmoq umuman ishlatilmaydi. Shu sabab API testlari millisekundlarda ishlaydi.
Yig'ib aytganda: test piramidasi β ko'p mayda birlik testlari (alohida funksiyalar, tez), o'rtacha integratsiya testlari (
TestClientkabi, bir nechta qism birga), kam uchidan-uchiga testlar (butun tizim). Mock vaTestClientβ shu piramidaning poydevorini mustahkam tutadi.
βοΈ Masalalar (26 ta)¶
Bu masalalarda funksiya yozib, keyin uning testini yoz.
pytestni terminalda ishga tushirib tekshir.
Oson (1β7):
kop(a, b)funksiyasini yoz vatest_koptestini yoz (assertbilan).juftmi(n)yoz; juft va toq holat uchun ikkita test yoz.salom(ism)yoz (matn qaytarsin); test'da natijada ism borliginiinbilan tekshir.- Ro'yxat qaytaruvchi funksiya yoz; test'da uzunligini va birinchi elementni tekshir.
assertbilan oddiy holatlarni tekshir:2+2==4,"a" in "abc",len([1,2])==2.- Oddiy
@pytest.fixtureyoz (lug'at qaytarsin), uni testda ishlat. @pytest.mark.parametrizebilanqosh(a,b)ni 3 xil holatda test qil.
O'rta (8β14):
bol(a, b)(nolga bo'lgandaValueError) yoz,pytest.raisesbilan test qil.- Normal holatni va xato holatini ikki alohida test bilan tekshir.
kvadrat(n)funksiyasiniparametrizebilan 4 ta holatda sina.- Fixture yoz: talabalar ro'yxati qaytarsin, uni bir nechta testda ishlat.
ortacha(sonlar)yoz; bo'sh ro'yxat berilgandaZeroDivisionErrorchiqishini test qil.palindrommi(soz)yoz;parametrizebilan palindrom va emas holatlarni test qil.eng_katta(sonlar)yoz; turli ro'yxatlar bilanparametrizeorqali test qil.
Murakkab (15β20):
Hisobklassini (5-moduldagi bank hisobi) yozib,qoyvayechmetodlarini test qil (fixture bilan yangi hisob yarat).yechda yetarli mablag' bo'lmasa xato chiqishinipytest.raisesbilan test qil.slugify(matn)yoz ("Salom Dunyo"β"salom-dunyo");parametrizebilan turli holatlarni test qil.Kalkulyatorklassini yozib (qosh,ayir,kop,bol), barcha metodlarni test bilan qopla (nolga bo'lish ham).- Fixture kompozitsiyasi: bir fixture (bo'sh savat) va undan foydalanadigan boshqa fixture (to'la savat) yoz, ikkalasini testlarda ishlat.
- To'liq test to'plami:
Talabaklassi (ball_qosh,ortacha) uchun normal holatlar, chegara holatlar (bo'sh ballar) va xato holatlarini qoplaydigan testlar yoz.
Mock, monkeypatch va zamonaviy vositalar (21β26):
foydalanuvchi_ismi(api)yoz β uapi.olish(1)ni chaqirib, qaytgan lug'atdan"ism"ni qaytarsin.Mockbilan soxtaapiyarat vaassert_called_once_withbilan to'g'ri chaqirilganini tekshir.rejim()yoz βos.environdan"REJIM"ni o'qisin, bo'lmasa"ishlab-chiqarish"qaytarsin.monkeypatchbilan ikkala holatni (o'rnatilgan va o'rnatilmagan) test qil.aylana_yuzasi(r)yoz (pi * r**2); natijanipytest.approxbilan tekshir (float ekanini esla).hisobotni_saqla(yol, sonlar)yoz β yig'indini faylga yozsin vaprintqilsin.tmp_path(fayl) vacapsys(chiqish) bilan birga test qil.- Bitta testni
@pytest.mark.skipifbilan (masalan, platformaga qarab) o'tkazib yubor, boshqasini@pytest.mark.xfailbilan (float bug) belgila. malumot_ol(url, ochuvchi)yoz βochuvchi(url)ni chaqirsin,URLErrorchiqsa"ulanish yo'q"qaytarsin.Mock(side_effect=...)bilan tarmoq uzilishini taqlid qilib test qil.
β Yechimlar¶
Ko'rsatish uchun ochish
import pytest
# 1
def kop(a: int, b: int) -> int:
return a * b
def test_kop():
assert kop(3, 4) == 12
# 2
def juftmi(n: int) -> bool:
return n % 2 == 0
def test_juft():
assert juftmi(4) is True
def test_toq():
assert juftmi(3) is False
# 3
def salom(ism: str) -> str:
return f"Salom, {ism}!"
def test_salom():
assert "Aziz" in salom("Aziz")
# 4
def sonlar() -> list[int]:
return [10, 20, 30]
def test_sonlar():
r = sonlar()
assert len(r) == 3
assert r[0] == 10
# 5
def test_oddiy():
assert 2 + 2 == 4
assert "a" in "abc"
assert len([1, 2]) == 2
# 6
@pytest.fixture
def user():
return {"ism": "Vali", "rol": "admin"}
def test_user(user):
assert user["rol"] == "admin"
# 7
@pytest.mark.parametrize("a, b, kutilgan", [(1, 1, 2), (5, 5, 10), (-1, 1, 0)])
def test_qosh(a, b, kutilgan):
assert a + b == kutilgan
# 8
def bol(a: float, b: float) -> float:
if b == 0:
raise ValueError("nolga bo'lish mumkin emas")
return a / b
def test_bol_nol():
with pytest.raises(ValueError):
bol(10, 0)
# 9
def test_normal():
assert bol(10, 2) == 5
def test_xato():
with pytest.raises(ValueError):
bol(5, 0)
# 10
def kvadrat(n: int) -> int:
return n ** 2
@pytest.mark.parametrize("n, kutilgan", [(2, 4), (3, 9), (4, 16), (0, 0)])
def test_kvadrat(n, kutilgan):
assert kvadrat(n) == kutilgan
# 11
@pytest.fixture
def talabalar():
return [{"ism": "Aziz", "ball": 85}, {"ism": "Gul", "ball": 70}]
def test_talabalar_soni(talabalar):
assert len(talabalar) == 2
def test_birinchi(talabalar):
assert talabalar[0]["ism"] == "Aziz"
# 12
def ortacha(sonlar: list[float]) -> float:
return sum(sonlar) / len(sonlar)
def test_ortacha_bosh():
with pytest.raises(ZeroDivisionError):
ortacha([])
# 13
def palindrommi(soz: str) -> bool:
return soz == soz[::-1]
@pytest.mark.parametrize("soz, kutilgan", [("radar", True), ("salom", False), ("aba", True)])
def test_palindrom(soz, kutilgan):
assert palindrommi(soz) == kutilgan
# 14
def eng_katta(sonlar: list[int]) -> int:
return max(sonlar)
@pytest.mark.parametrize("sonlar, kutilgan", [([1, 5, 3], 5), ([10], 10), ([-1, -5], -1)])
def test_eng_katta(sonlar, kutilgan):
assert eng_katta(sonlar) == kutilgan
# 15
class Hisob:
def __init__(self, balans: float = 0) -> None:
self.balans = balans
def qoy(self, s: float) -> None:
self.balans += s
def yech(self, s: float) -> None:
if s > self.balans:
raise ValueError("mablag' yetarli emas")
self.balans -= s
@pytest.fixture
def hisob():
return Hisob(100)
def test_qoy(hisob):
hisob.qoy(50)
assert hisob.balans == 150
def test_yech(hisob):
hisob.yech(40)
assert hisob.balans == 60
# 16
def test_yech_xato(hisob):
with pytest.raises(ValueError):
hisob.yech(1000)
# 17
import re
def slugify(matn: str) -> str:
matn = matn.strip().lower()
matn = re.sub(r"[^\w\s-]", "", matn)
return re.sub(r"[\s]+", "-", matn)
@pytest.mark.parametrize("kirish, kutilgan", [
("Salom Dunyo", "salom-dunyo"),
("Python", "python"),
("Bir Ikki Uch", "bir-ikki-uch"),
])
def test_slugify(kirish, kutilgan):
assert slugify(kirish) == kutilgan
# 18
class Kalkulyator:
def qosh(self, a: float, b: float) -> float: return a + b
def ayir(self, a: float, b: float) -> float: return a - b
def kop(self, a: float, b: float) -> float: return a * b
def bol(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("nolga bo'lish")
return a / b
@pytest.fixture
def kalk():
return Kalkulyator()
def test_qosh_k(kalk):
assert kalk.qosh(2, 3) == 5
def test_bol_k(kalk):
assert kalk.bol(10, 2) == 5
def test_bol_nol_k(kalk):
with pytest.raises(ValueError):
kalk.bol(1, 0)
# 19
@pytest.fixture
def bosh_savat():
return []
@pytest.fixture
def tola_savat(bosh_savat):
bosh_savat.append("olma")
bosh_savat.append("non")
return bosh_savat
def test_bosh(bosh_savat):
assert len(bosh_savat) == 0
def test_tola(tola_savat):
assert len(tola_savat) == 2
# 20
class Talaba:
def __init__(self, ism: str) -> None:
self.ism = ism
self.ballar: list[int] = []
def ball_qosh(self, b: int) -> None:
self.ballar.append(b)
def ortacha(self) -> float:
if not self.ballar:
raise ValueError("ballar yo'q")
return sum(self.ballar) / len(self.ballar)
@pytest.fixture
def talaba():
return Talaba("Aziz")
def test_ball_qosh(talaba):
talaba.ball_qosh(80)
assert talaba.ballar == [80]
def test_ortacha(talaba):
talaba.ball_qosh(80)
talaba.ball_qosh(90)
assert talaba.ortacha() == 85
def test_ortacha_bosh(talaba):
with pytest.raises(ValueError):
talaba.ortacha()
# 21 β Mock
from unittest.mock import Mock, patch
def foydalanuvchi_ismi(api) -> str:
malumot = api.olish(1)
return malumot["ism"]
def test_foydalanuvchi_ismi():
soxta_api = Mock()
soxta_api.olish.return_value = {"ism": "Laylo", "yosh": 22}
assert foydalanuvchi_ismi(soxta_api) == "Laylo"
soxta_api.olish.assert_called_once_with(1)
# 22 β monkeypatch (env)
import os
def rejim() -> str:
return os.environ.get("REJIM", "ishlab-chiqarish")
def test_rejim_test(monkeypatch):
monkeypatch.setenv("REJIM", "test")
assert rejim() == "test"
def test_rejim_default(monkeypatch):
monkeypatch.delenv("REJIM", raising=False)
assert rejim() == "ishlab-chiqarish"
# 23 β approx
from math import pi
def aylana_yuzasi(r: float) -> float:
return pi * r ** 2
def test_aylana_yuzasi():
assert aylana_yuzasi(2) == pytest.approx(12.566, abs=0.001)
# 24 β tmp_path + capsys
def hisobotni_saqla(yol, sonlar: list[int]) -> None:
jami = sum(sonlar)
yol.write_text(f"Jami: {jami}", encoding="utf-8")
print(f"Saqlandi: {jami}")
def test_hisobotni_saqla(tmp_path, capsys):
fayl = tmp_path / "hisobot.txt"
hisobotni_saqla(fayl, [10, 20, 30])
assert fayl.read_text(encoding="utf-8") == "Jami: 60"
assert "Saqlandi: 60" in capsys.readouterr().out
# 25 β skipif + xfail
import sys
@pytest.mark.skipif(sys.platform == "darwin", reason="bu test mac uchun emas")
def test_platforma():
assert 1 + 1 == 2
@pytest.mark.xfail(reason="float aniqligi β ma'lum holat")
def test_float_bug():
assert 0.1 + 0.2 == 0.3
# 26 β Mock side_effect (tarmoq xatosi)
import urllib.error
def malumot_ol(url: str, ochuvchi) -> str:
try:
return ochuvchi(url)
except urllib.error.URLError:
return "ulanish yo'q"
def test_malumot_ol_xato():
soxta = Mock(side_effect=urllib.error.URLError("timeout"))
assert malumot_ol("http://x", soxta) == "ulanish yo'q"
β Parallel ishlash | Boshlovchilar README β | Keyingi: FastAPI veb API β