30 β Kapston: loyihani noldan test bilan qoplash¶
π README Β· β¬ οΈ Oldingi: 29 β Eski (legacy) kodni testlash
Bu bobda: butun kitobni bitta amaliy loyihada birlashtiramiz. Kichik, lekin realistik xizmatni β kutubxona / kitob ijarasi tizimini β noldan, strategiyadan boshlab, unit β dublyor β integratsiya β API β E2E testlargacha, sifat darvozalari (coverage, mutation g'oyasi) bilan to'liq qoplaymiz. Har bosqichda tegishli bobga ishora qilamiz. Yakunda haqiqiy test to'plamini ishga tushiramiz va real
passed+ coverage hisobotini ko'ramiz.Halollik / Eslatma: bu β namuna loyiha, qisqalik uchun soddalashtirilgan (bitta jadval-juftlik, in-memory
sqlite, dict-asosli API). Maqsad β to'liq korxona kodi emas, balki fikrlash oqimini boshdan oxirigacha ko'rsatish. Barcha kod va sonlarpython -m pytest(Python 3.14, pytest 9.0.3, coverage 7.14.1) bilan haqiqatan ishga tushirib olingan β# ->,passedva coverage foizi to'qib chiqarilmagan.
Loyiha: kutubxona / kitob ijarasi¶
Tasavvur qiling, mahalla kutubxonasi uchun kichik xizmat yozyapsiz. Talablar oddiy:
- Foydalanuvchi kitobni ijaraga oladi (standart muddat β 14 kun).
- Bir foydalanuvchi bir vaqtda eng ko'pi 3 ta kitob ushlay oladi.
- Kitobning nusxasi tugagan bo'lsa, ijaraga berilmaydi.
- Kechikib qaytarsa, har kun uchun 1000 so'm jarima β uni tashqi to'lov servisi orqali yechamiz.
- Hammasi HTTP API orqali ishlatiladi.
Bu kichik talablar to'plamida butun kitob mavzulari mujassam: sof domen mantig'i (jarima, limit), ma'lumotlar bazasi (kitob/ijara saqlash), tashqi servis (to'lov β sizniki emas), HTTP qatlam. Aynan shu sabab bu loyihani tanladik.
Tizimni to'rt qatlamga ajratamiz (10-bobdagi testlanadigan dizayn β portlar/adapterlar):
| Qatlam | Fayl | Mas'uliyat | Test turi (bob) |
|---|---|---|---|
| Domen | domen.py |
sof qoidalar: jarima, limit | unit (02β06) |
| Ombor | ombor.py |
sqlite repozitoriy | integratsiya (15β16) |
| To'lov porti | tolov.py |
tashqi servis interfeysi | fake/stub (07β10) |
| Xizmat | xizmat.py |
use-case'larni birlashtiradi | dublyor bilan (08) |
| API | api.py |
so'rov β (status, javob) | API + E2E (17β19) |
Eslatma β "DB sizniki, to'lov sizniki emas". Bu kapstonning markaziy g'oyasi (16-bob). Ma'lumotlar bazasini biz real sinaymiz (xatolar faqat real DB'da ko'rinadi). To'lov servisi esa bizniki emas β uni izolyatsiya qilamiz (real pul yechilmasin). Shu farqni butun bob davomida ko'rasiz.
1-bosqich: strategiya (28-bob)¶
Birinchi assertdan oldin reja tuzamiz. 28-bobdagi risk-asosli yondashuv savol beradi:
nimani, qaysi darajada, nega shu darajada testlaymiz? Har narsani bir xil qattiq testlash β
isrof; hech narsani testlamaslik β xavf. Risk = ehtimol Γ ta'sir bo'yicha ustuvorlik qo'yamiz:
| Komponent | Xato ehtimoli | Ta'sir | Reja |
|---|---|---|---|
| Jarima hisoblash | O'rta (chegara xatosi) | Yuqori (pul!) | Ko'p unit + chegara |
| Ijara limiti | O'rta | O'rta | Unit (parametrize) |
| DB so'rovlari | Yuqori (SQL/cheklov) | Yuqori | Integratsiya (real sqlite) |
| To'lov integratsiyasi | Past (port soddalashtiradi) | Yuqori | Dublyor (fake) + bitta E2E |
| API marshrutlash | Past | O'rta | Bir nechta API testi |
| To'liq oqim | β | Yuqori | 1 E2E smoke |
Bu jadval bevosita test piramidasiga aylanadi (03-bob): pastda ko'p tez unit, o'rtada bir nechta integratsiya/API, tepada bitta E2E. Pul bilan bog'liq mantiq (jarima) eng zich qoplanadi β bu risk-asosli tanlov.
Trade-off: strategiya β bu "nima yozaman" emas, "nima yozMAYman" haqida ham. To'lov servisining ichini (real HTTP, retry) bu loyihada testlamaymiz β u bizniki emas, port orqali izolyatsiya qilamiz. Buni testlash = boshqa jamoaning kodini testlash; ROI past (28-bob).
2-bosqich: domen mantig'ini unit test bilan (02/04/05/06-bob)¶
Eng pastdan β sof funksiyalardan boshlaymiz. Sof funksiya (DB, vaqt, tarmoqsiz) β testlash uchun jannat: kirish bersangiz, chiqishini tekshirasiz, xolos (10-bob).
# kutubxona/domen.py
KUNLIK_JARIMA = 1000 # kechikkan har kun uchun (so'm)
MAKS_KITOB = 3 # bir foydalanuvchi bir vaqtda nechta kitob olishi mumkin
def jarima_hisobla(muddat_kuni, qaytarilgan_kuni):
kechikish = qaytarilgan_kuni - muddat_kuni
if kechikish <= 0:
return 0 # vaqtida yoki erta -> jarima yo'q
return kechikish * KUNLIK_JARIMA
def ijara_mumkinmi(hozirgi_soni, kitob_bormi):
if not kitob_bormi:
return False
return hozirgi_soni < MAKS_KITOB
Endi parametrize (06-bob) bilan ko'p holatni β ayniqsa chegara qiymatlarini (05-bob) β bitta jadvalda sinaymiz. Chegara β eng ko'p xato uchun joy: aniq muddat kuni, aniq limit.
# testlar/test_domen.py
import pytest
from kutubxona.domen import jarima_hisobla, ijara_mumkinmi, KUNLIK_JARIMA, MAKS_KITOB
@pytest.mark.parametrize("muddat, qaytarilgan, kutilgan", [
(14, 14, 0), # chegara: aniq vaqtida
(14, 10, 0), # erta qaytdi
(14, 15, KUNLIK_JARIMA), # 1 kun kechikdi
(14, 17, 3 * KUNLIK_JARIMA), # 3 kun kechikdi
(0, 0, 0), # chegara: nol kun
(14, 30, 16 * KUNLIK_JARIMA), # uzoq kechikish: 16 kun
(30, 5, 0), # ancha erta qaytdi (manfiy kechikish)
])
def test_jarima_hisobla(muddat, qaytarilgan, kutilgan):
assert jarima_hisobla(muddat, qaytarilgan) == kutilgan
@pytest.mark.parametrize("soni, bormi, kutilgan", [
(0, True, True), # bo'sh, kitob bor
(MAKS_KITOB - 1, True, True), # limitdan biroz past
(MAKS_KITOB, True, False), # chegara: limitga yetdi
(MAKS_KITOB + 1, True, False), # limitdan oshib ketgan
(1, False, False), # kitob yo'q
])
def test_ijara_mumkinmi(soni, bormi, kutilgan):
assert ijara_mumkinmi(soni, bormi) is kutilgan
Ishga tushiramiz β har parametr alohida test sifatida sanaladi:
12 ta sof, millisekundlik test. Bu β piramidaning keng asosi. Hech qanday DB, hech qanday mock β shuning uchun ular AAA'ning eng toza ko'rinishi va FIRST'ning hammasiga (tez, izolyatsiyalangan, takrorlanadigan) javob beradi (02, 04-bob).
3-bosqich: test dublyorlari β to'lov servisini izolyatsiya (07/08/10-bob)¶
To'lov servisini testda chaqirib bo'lmaydi: real pul yechiladi. 10-bobdagi port/adapter naqshini qo'llaymiz β kod faqat interfeys orqali to'lov bilan gaplashadi, testda u joyga fake (07-bob) qo'yamiz.
# kutubxona/tolov.py
from typing import Protocol
class TolovPorti(Protocol):
def tola(self, foydalanuvchi: str, summa: int) -> bool: ...
class FakeTolov: # test dublyori
def __init__(self, muvaffaqiyat=True):
self.muvaffaqiyat = muvaffaqiyat
self.yuborilgan = [] # spy: chaqiruvlarni qayd qiladi
def tola(self, foydalanuvchi, summa):
self.yuborilgan.append((foydalanuvchi, summa))
return self.muvaffaqiyat
FakeTolov β ham fake (ishlaydigan, lekin soddalashtirilgan), ham spy (chaqiruvlarni
yozib boradi). Xizmat klassi to'lov portini konstruktor orqali oladi (DI, 10-bob) β aynan shu
narsa uni testlanadigan qiladi:
# kutubxona/xizmat.py (qisqartirilgan)
class KutubxonaXizmati:
def __init__(self, kitob_ombori, ijara_ombori, tolov):
self.kitoblar, self.ijaralar, self.tolov = kitob_ombori, ijara_ombori, tolov
def qaytar(self, ijara_id, qaytarilgan_kuni):
ijara = self.ijaralar.topish(ijara_id)
if ijara is None:
raise IjaraXatosi("Ijara topilmadi")
if ijara["qaytarilgan"]:
raise IjaraXatosi("Allaqachon qaytarilgan")
jarima = jarima_hisobla(ijara["muddat_kuni"], qaytarilgan_kuni)
if jarima > 0:
if not self.tolov.tola(ijara["foydalanuvchi"], jarima):
raise IjaraXatosi("Jarima to'lovi amalga oshmadi")
self.ijaralar.qaytarilgan_deb_belgila(ijara_id)
self.kitoblar.nusxa_ozgartir(ijara["kitob_id"], +1)
return jarima
Testda fake to'lov bilan ikki muhim narsani tasdiqlaymiz: (1) jarima bo'lsa to'lov chaqirildi; (2) jarima yo'q bo'lsa chaqirilmadi (07-bobdagi xulq-atvor tekshiruvi):
# testlar/test_xizmat.py (qisqacha)
def test_kechikkanda_jarima_tolanadi(xizmat, ulanish, tolov):
kitob_id = KitobOmbori(ulanish).qosh("Refactoring", 1)
ijara_id = xizmat.ijaraga_ber("ali", kitob_id, muddat_kuni=14)
jarima = xizmat.qaytar(ijara_id, qaytarilgan_kuni=17) # 3 kun kech
assert jarima == 3000
assert tolov.yuborilgan == [("ali", 3000)] # to'lov chaqirildi
def test_vaqtida_qaytarsa_tolov_chaqirilmaydi(xizmat, ulanish, tolov):
kitob_id = KitobOmbori(ulanish).qosh("Clean Code", 1)
ijara_id = xizmat.ijaraga_ber("vali", kitob_id, muddat_kuni=14)
assert xizmat.qaytar(ijara_id, qaytarilgan_kuni=14) == 0
assert tolov.yuborilgan == [] # jarima yo'q -> to'lov yo'q
To'lov yiqilsa nima bo'lishini ham sinaymiz β FakeTolov(muvaffaqiyat=False) bilan istisno
kutamiz (05-bob):
def test_tolov_amalga_oshmasa_xato(ulanish):
yiqilgan = FakeTolov(muvaffaqiyat=False)
x = KutubxonaXizmati(KitobOmbori(ulanish), IjaraOmbori(ulanish), yiqilgan)
kid = KitobOmbori(ulanish).qosh("DDD", 1)
ijara_id = x.ijaraga_ber("guli", kid, muddat_kuni=14)
with pytest.raises(IjaraXatosi, match="to'lovi amalga oshmadi"):
x.qaytar(ijara_id, qaytarilgan_kuni=20)
# -> 1 passed
Diqqat β ortiqcha mock'lamang (08-bob). Bu yerda faqat to'lovni dublyor qildik. DB'ni dublyor qilmadik β uni keyingi bosqichda real sinaymiz. Hammasini mock qilsangiz, testlaringiz faqat o'z taxminlaringizni tasdiqlaydi (16-bob).
4-bosqich: integratsiya + ma'lumotlar bazasi (15/16-bob)¶
Endi real sqlite. Ombor β repozitoriy naqshi: DB mantig'i bir joyda. Eng muhim mahorat β
izolyatsiya: har test toza holatdan boshlasin. 16-bobdagi tranzaksiya-rollback strategiyasini
conftest.pyda fixture sifatida yozamiz:
# conftest.py (qisqacha)
@pytest.fixture(scope="session")
def ulanish_session():
ulanish = sqlite3.connect(":memory:")
ulanish.row_factory = sqlite3.Row
migratsiya(ulanish) # sxema bir marta quriladi
yield ulanish
ulanish.close()
@pytest.fixture
def ulanish(ulanish_session):
ulanish_session.execute("BEGIN") # setup: tranzaksiya och
yield ulanish_session
ulanish_session.rollback() # teardown: hammani bekor qil
Sxemada cheklovlar bor β aynan ularni mock qilingan DB hech qachon ushlamaydi (16-bob):
# kutubxona/ombor.py β sxemadan parcha
# CREATE TABLE kitoblar (id ..., nom TEXT NOT NULL, nusxa INTEGER CHECK (nusxa >= 0));
Repozitoriyni real DB bilan sinaymiz, va CHECK cheklovi haqiqatan ishlashini ko'rsatamiz:
# testlar/test_ombor.py (qisqacha)
def test_kitob_qosh_va_topish(ulanish):
ombor = KitobOmbori(ulanish)
kid = ombor.qosh("Test Driven Development", 3)
assert ombor.topish(kid) == {"id": kid, "nom": "Test Driven Development", "nusxa": 3}
def test_manfiy_nusxa_check_buziladi(ulanish):
ombor = KitobOmbori(ulanish)
kid = ombor.qosh("Working Effectively", 0)
with pytest.raises(sqlite3.IntegrityError):
ombor.nusxa_ozgartir(kid, -1) # CHECK (nusxa >= 0) -> xato
Izolyatsiyani ikki test bilan isbotlaymiz (16-bob): birinchisi qator qo'shadi, ikkinchisi β agar rollback ishlasa β bo'sh jadvaldan boshlanishi shart:
def test_izolyatsiya_birinchi(ulanish):
KitobOmbori(ulanish).qosh("Faqat shu test", 1)
assert ulanish.execute("SELECT COUNT(*) FROM kitoblar").fetchone()[0] == 1
def test_izolyatsiya_ikkinchi(ulanish):
assert ulanish.execute("SELECT COUNT(*) FROM kitoblar").fetchone()[0] == 0 # bo'sh!
test_izolyatsiya_ikkinchi bo'sh jadvaldan boshlandi β demak rollback ishlaydi. Testlarni istalgan
tartibda ishga tushiring, natija o'zgarmaydi (FIRST β Independent). Bu β flaky'dan qochishning asosi
(24-bob).
Trade-off β realistiklik vs tezlik (16-bob). In-memory sqlite tez, lekin production PostgreSQL'dan lahjasi farq qiladi. Ko'p loyiha ikkalasini ishlatadi: lokal tezlik uchun in-memory, CI'da ishonch uchun production-bilan-bir-xil DB (15-bob, Testcontainers).
5-bosqich: TDD bilan yangi talab qo'shamiz (11/12-bob)¶
Loyiha tayyor turibdi va yangi talab keldi: "bir ijarani ikki marta qaytarib bo'lmaydi". Buni 11-bobdagi Red-Green-Refactor sikli bilan qo'shamiz β avval test, keyin kod.
π΄ Red β yiqiladigan testni yozamiz (kod hali yo'q):
def test_ikki_marta_qaytarib_bolmaydi(xizmat, ulanish):
kid = KitobOmbori(ulanish).qosh("xUnit", 1)
ijara_id = xizmat.ijaraga_ber("davron", kid, muddat_kuni=14)
xizmat.qaytar(ijara_id, qaytarilgan_kuni=14) # 1-marta: ok
with pytest.raises(IjaraXatosi, match="Allaqachon qaytarilgan"):
xizmat.qaytar(ijara_id, qaytarilgan_kuni=14) # 2-marta: rad etilsin
Ishga tushiramiz β test yiqiladi (himoya hali yo'q, ikkinchi qaytar ham o'tib ketadi):
π’ Green β eng kichik kod bilan o'tkazamiz. qaytarga ikki qator guard qo'shamiz:
π΅ Refactor β toza qilamiz. Bu holatda kod allaqachon ozoda (boshqa guard'lar bilan bir uslubda), shuning uchun qo'shimcha tozalash shart emas. Eski testlar hali yashil β bu regressiya yo'qligini isbotlaydi (1-bob). TDD'ning kuchi shu: yangi talab avval spetsifikatsiya (test) bo'lib yoziladi, keyin kod uni qondiradi.
Eslatma (11-bob): TDD β test yozish texnikasi emas, dizayn texnikasi. "Avval test" sizni "bu kodni qanday chaqiraman?" deb o'ylashga majbur qiladi β natijada interfeys tabiiyroq bo'ladi.
6-bosqich: API/kontrakt va E2E smoke (17/18/19-bob)¶
API qatlami β yupqa (humble object, 10-bob): u faqat so'rovni xizmatga uzatadi va natijani status kodga aylantiradi. Bu yerda biz shartnomani (kontrakt, 18-bob) tekshiramiz: qaysi so'rov qaysi statusni beradi.
# kutubxona/api.py (qisqacha) β so'rov dict -> (status, javob dict)
def post_ijara(self, sorov):
try:
ijara_id = self.xizmat.ijaraga_ber(
sorov["foydalanuvchi"], sorov["kitob_id"], sorov["muddat_kuni"])
return 201, {"ijara_id": ijara_id}
except KeyError as x:
return 400, {"xato": f"maydon yetishmaydi: {x}"}
except IjaraXatosi as x:
return 409, {"xato": str(x)}
# testlar/test_api.py (qisqacha) β status kod shartnomasi (17-bob)
def test_post_ijara_201(api, ulanish):
kid = KitobOmbori(ulanish).qosh("BDD", 1)
status, javob = api.post_ijara({"foydalanuvchi": "ali", "kitob_id": kid, "muddat_kuni": 14})
assert status == 201 and "ijara_id" in javob
def test_post_ijara_maydon_yetishmaydi_400(api):
status, javob = api.post_ijara({"foydalanuvchi": "ali"}) # kitob_id yo'q
assert status == 400 and "maydon yetishmaydi" in javob["xato"]
Eslatma (17-bob, til-mustaqil): real loyihada bu yerda haqiqiy HTTP bo'ladi β Python'da
requests/httpx+ mock server (responses), JS'dasupertest, PHP'da Laravel/Symfony test mijozi. So'rov/javob, status kod, sxema validatsiyasi g'oyasi bir xil. Biz HTTP karkasini soddalashtirib, mantiqiy shartnomaga e'tibor qildik.
Va nihoyat bitta E2E smoke test (19-bob) β piramidaning uchi. U butun tizimni boshidan oxirigacha bir oqimda yuradi: katalogga kitob β API orqali ijara β kechikib qaytarish β jarima to'lovi β nusxa qaytishi:
# testlar/test_e2e.py
def test_toliq_ijara_oqimi(api, ulanish, tolov):
kid = KitobOmbori(ulanish).qosh("Kapston kitobi", 1) # 1. seed
status, javob = api.post_ijara( # 2. ijara
{"foydalanuvchi": "nodira", "kitob_id": kid, "muddat_kuni": 14})
assert status == 201
assert KitobOmbori(ulanish).topish(kid)["nusxa"] == 0 # nusxa band
status, qaytar = api.post_qaytar( # 3. kech qaytarish
{"ijara_id": javob["ijara_id"], "qaytarilgan_kuni": 19}) # 5 kun kech
assert status == 200
assert qaytar["jarima"] == 5000
assert tolov.yuborilgan == [("nodira", 5000)] # to'lov amalga oshdi
assert KitobOmbori(ulanish).topish(kid)["nusxa"] == 1 # nusxa qaytdi
E2E kam bo'lishi kerak: u sekin, mo'rt va "nima buzildi"ni aniq aytmaydi. Lekin bitta bunday test β barcha qatlamlar birga to'g'ri ishlashiga ishonch beradi. Bu β piramidaning to'g'ri shakli.
7-bosqich: sifat darvozalari β coverage, mutation, flaky (20/22/24-bob)¶
To'plamni yaxlit ishga tushiramiz va coverage (20-bob) o'lchaymiz:
$ python -m coverage run --source=kutubxona -m pytest -q
................................ [100%]
32 passed in 0.59s
$ python -m coverage report -m
Name Stmts Miss Cover Missing
-----------------------------------------------------
kutubxona\__init__.py 0 0 100%
kutubxona\api.py 20 0 100%
kutubxona\domen.py 17 0 100%
kutubxona\ombor.py 29 0 100%
kutubxona\tolov.py 9 0 100%
kutubxona\xizmat.py 34 0 100%
-----------------------------------------------------
TOTAL 109 0 100%
32 test, 100% coverage, 0.6 soniya. Ammo 20-bobning asosiy saboq: 100% coverage β xatosiz kod. Coverage faqat "qator ishga tushdimi" deydi, "to'g'ri tekshirildimi" demaydi (Goodhart qonuni).
Buni mutation testing (22-bob) bilan ko'rsatamiz. Kodga ataylab kichik o'zgarish (mutant) kiritib, testlar uni ushlaydimi tekshiramiz. Ikki haqiqiy tajriba o'tkazdim:
Mutant A β < MAKS_KITOB ni <= MAKS_KITOB ga o'zgartirdim (limitni bitta oshirib yuboradi):
Test yiqildi β mutant o'ldirildi (killed). Bu β chegara testimiz (MAKS_KITOB, True, False)
ish berayotganini isbotlaydi. β
Mutant B β if kechikish <= 0 ni if kechikish < 0 ga o'zgartirdim:
Hamma test o'tib ketdi β mutant omon qoldi (survived)! β Nega? Chunki kechikish == 0 holatida
yangi kod 0 * KUNLIK_JARIMA = 0 qaytaradi β natija baribir 0. Coverage 100% bo'lsa-da, bu yashirin
zaiflik. Bu mutation testingning butun qiymati: u coverage ko'rsatmaydigan bo'shliqni topadi.
Halol gap: omon qolgan mutant bizga "yana bitta tekshiruv qo'shsam yaxshi bo'lardi" deyapti (masalan,
jarima_hisoblaichki shartining aniq xulqi). Demak hatto chiroyli, 100%-coverage'li to'plamning ham o'sish joyi bor β bu normal va sog'lom signal (22-bob).
Flaky'dan qochish (24-bob): butun to'plam deterministik β vaqt/tasodif/tarmoq tashqariga chiqarilgan (to'lov fake, vaqt parametr sifatida uzatiladi), DB har test rollback bilan tozalanadi. Shuning uchun to'plamni istalgan tartibda va ko'p marta ishga tushiring β natija o'zgarmaydi.
CI'da ishga tushirish (27-bob, konseptual): har push'da to'plam avtomatik yuriydi. Yengil konfiguratsiya (YAML β til-mustaqil g'oya):
# .github/workflows/test.yml (g'oya)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.14" }
- run: pip install pytest coverage
- run: coverage run --source=kutubxona -m pytest -q
- run: coverage report --fail-under=90 # sifat darvozasi
--fail-under=90 β sifat darvozasi: coverage 90% dan tushsa, CI qizil bo'ladi va merge
to'xtaydi. Tez (sekundlar) bo'lgani uchun har commit'da yurita olamiz β bu tez fikr-mulohaza
sikli (27-bob).
Yakuniy natija: to'plam piramida shaklida¶
Butun to'plamni qatlamlar bo'yicha sanaymiz (haqiqiy --collect-only sonlari):
| Qatlam | Fayl | Test soni | Tezlik |
|---|---|---|---|
| Domen (unit) | test_domen.py |
12 | eng tez |
| Xizmat (dublyor) | test_xizmat.py |
7 | tez |
| Ombor (integratsiya, DB) | test_ombor.py |
6 | o'rta |
| API (kontrakt) | test_api.py |
6 | o'rta |
| E2E (smoke) | test_e2e.py |
1 | sekin |
| Jami | 32 | ~0.6s |
Guruhlab ko'rsak β tez (unit + dublyor) 19, integratsiya + API 12, E2E 1. Bu aynan sog'lom piramida: keng asos, ingichka uch (03-bob).
To'plam tez (millisekundlar emas, soniyaning yarmi) β chunki og'irligi pastda. Agar piramida teskari ("muzqaymoq", ko'p E2E) bo'lganida, u sekin va flaky bo'lardi. Shaklning o'zi β sifat ko'rsatkichi.
Testlash yo'l xaritasi: boshlovchidan ekspertgacha¶
Bu kitob safarini bitta rasmga jamladik. Har bosqich oldingisini o'chirmaydi β ustiga quradi:
- Boshlovchi β sintaksisga qaraydi.
assertyozadi, AAA biladi, "yashil = tugadi" deb o'ylaydi. Ko'pincha chegarani unutadi, hammasini mock qiladi, coverage'ni maqsad deb biladi. - O'rta β dizaynga qaraydi. Piramidani biladi, chegara/parametrize ishlatadi, TDD qila boshlaydi, DB'ni real sinaydi, flaky'ni tuzatadi, DI bilan testlanadigan kod yozadi.
- Ekspert β qiymat va riskka qaraydi. Risk-asosli strategiya tuzadi, mutation/property bilan testlarni sinaydi, kontrakt va CI darvozalarini quradi, eng muhimi β nimani test QILMASLIKni biladi, va jamoaga test madaniyatini olib kiradi.
Diqqat: bu yo'l darajalar emas, e'tibor markazining o'zgarishi. Sintaksisdan dizaynga, dizayndan qiymatga.
Keyingi qadamlar¶
O'qish (klassiklar):
- Kent Beck β "Test-Driven Development: By Example" β TDD ritmini ixtirochining o'zidan.
- Michael Feathers β "Working Effectively with Legacy Code" β seam, characterization test (29-bob).
- Gerard Meszaros β "xUnit Test Patterns" β dublyor taksonomiyasi va test smell'lar (07, 13-bob).
- Lisa Crispin & Janet Gregory β "Agile Testing" β testing quadrants va jamoa madaniyati (28-bob).
Amaliyot (eng muhimi):
- O'z loyihangizda bitta yangi feature'ni to'liq TDD bilan yozing (11β12-bob).
- Eski, testsiz koddan bitta characterization test bilan boshlang (29-bob).
- To'plamingizga mutation yoki property-based test qo'shib, yashirin bo'shliqlarni toping (21β22-bob).
Boshqa kitoblar:
- Dasturiy arxitektura β testlanadigan dizayn, portlar/adapterlar, qatlamlar chuqurroq.
- DevOps & Deployment β CI/CD pipeline'ida testlarni sozlash.
- Algoritmlar β to'g'rilik isboti va testning bir-birini to'ldirishi.
Yakun: qo'rquvni ishonchga aylantirish¶
Birinchi bobda aytgan edik: test β bu qo'rquvni ishonchga aylantiruvchi muhandislik intizomi.
Bu safar boshida sizda faqat bitta assert bor edi. Endi siz strategiya tuzasiz, piramida
quryapsiz, mantiqni izolyatsiya qilasiz, DB'ni real, tashqi servisni fake bilan sinaysiz,
yangi talabni TDD bilan haydaysiz, va testlaringizning kuchini mutation bilan tekshirasiz.
Eng muhim saboq texnik emas: test β yodlash emas, fikrlash tarzi. "Qaysi darajada? Nima uchun? Qaysi risk? Nimani test qilMASLIK?" β bu savollarni so'rashni o'rgandingiz. Til (Python, JS, PHP, Go) o'zgarsa ham, bu savollar va naqshlar bir xil qoladi.
Endi yo'lning haqiqiy qismi boshlanadi β o'z kodingiz. Kichik boshlang: bitta test, bitta characterization, bitta TDD sikli. Har test β kelajakdagi o'zingizga yozgan ishonch xati. Yaxshi yo'l!
Asosiy g'oyalar (bobni qisqacha)¶
- Strategiyadan boshla, koddan emas (28-bob): risk-asosli ustuvorlik β nimani qaysi darajada testlash. Pul mantiqi eng zich qoplanadi.
- Pastdan yuqoriga qur: sof domen unit β dublyor bilan xizmat β real DB integratsiya β API/kontrakt β bitta E2E. Bu tabiiy piramida shaklini beradi.
- "DB sizniki, to'lov sizniki emas" (16-bob): ma'lumotlar bazasini real sina, tashqi servisni port + fake bilan izolyatsiya qil.
- DI = testlanadigan dizayn (10-bob): to'lov porti konstruktor orqali uzatilgani uchun fake qo'yish oson. Dizayn testni osonlashtiradi.
- Izolyatsiya β flaky'dan qochishning asosi (16, 24-bob): tranzaksiya-rollback har testni toza holatdan boshlatadi; to'plam tartibga bog'liq emas.
- TDD bilan yangi talab (11-bob): Red (yiqiladigan test) β Green (eng kichik kod) β Refactor. Eski testlar yashil qolishi = regressiya yo'q.
- 100% coverage β xatosiz (20, 22-bob): mutant B omon qoldi β coverage ko'rsatmagan bo'shliqni mutation testing topdi. Coverage darvoza, maqsad emas.
- Piramida shakli = sifat ko'rsatkichi: 19 tez / 12 integratsiya / 1 E2E. Keng asos = tez, ishonchli to'plam. Yakun: test β fikrlash tarzi.
Mashqlar¶
Oson¶
1-mashq. Kapston loyihasi qaysi to'rt qatlamdan iborat va har qatlam qaysi test turi bilan qoplanadi? "DB sizniki, to'lov sizniki emas" iborasini o'z so'zingiz bilan tushuntiring.
2-mashq. To'plamning yakuniy piramidasini sonlar bilan yozing (qaysi qatlamda nechta test). Nega E2E eng kam, unit eng ko'p bo'lishi kerak?
3-mashq. test_vaqtida_qaytarsa_tolov_chaqirilmaydi testida nega tolov.yuborilgan == [] ni
tekshiramiz? Bu qanaqa dublyor turi (07-bob) va qaysi narsani isbotlaydi?
O'rta¶
4-mashq. Risk-asosli strategiya jadvalini (28-bob) yangi talab uchun to'ldiring: "foydalanuvchi o'zining ijaralar tarixini ko'radi". Xato ehtimoli, ta'siri va test rejasini (daraja + son) yozing.
5-mashq. jarima_hisobla uchun omon qolgan mutant (<= 0 β < 0) ni o'ldiradigan yangi
test o'ylab toping. Maslahat: shartning ikki tomonini ajrata oladigan kirish kerak. Nega mavjud
testlar uni ushlamadi?
6-mashq. "Bekor qilingan ijarani qaytarib bo'lmaydi" talabini TDD bilan qo'shing: avval Red
(yiqiladigan test), keyin Green (eng kichik kod), keyin Refactor bosqichlarini yozing. (Ijaraga
bekor_qilingan maydoni borligini faraz qiling.)
Qiyin¶
7-mashq. E2E smoke testga yangi senariy qo'shing: foydalanuvchi 3 ta kitob oladi, 4-chisini
olmoqchi bo'lganda API 409 (limit) qaytarishini, bitta kitobni qaytargach yana ola olishini
to'liq oqimda tekshiring. Qaysi qatlamlar ishtirok etadi?
8-mashq. Hozir piramida 19/12/1. Tasavvur qiling, kimdir uni "teskari" qildi: 1 unit, 5 integratsiya, 20 E2E. Bunday to'plamning uchta aniq muammosini ayting (tezlik, ishonch, diagnostika nuqtai nazaridan) va nega bu "muzqaymoq" anti-naqsh ekanini tushuntiring (03-bob).
Yechimlar
1-mashq yechimi¶
To'rt qatlam: (1) Domen (domen.py) β sof qoidalar (jarima, limit), unit test bilan; (2) Ombor
(ombor.py) β sqlite repozitoriy, integratsiya testi bilan; (3) To'lov porti (tolov.py) β tashqi
servis interfeysi, fake/stub bilan; (4) Xizmat + API β use-case'lar va HTTP qatlam, dublyor + E2E
bilan. "DB sizniki, to'lov sizniki emas": ma'lumotlar bazasi sizning mas'uliyatingiz β uni real sinash
kerak (xatolar faqat real DB'da ko'rinadi); to'lov servisi boshqa tomonniki β uni real chaqirib bo'lmaydi
(pul yechiladi), shuning uchun port + fake bilan izolyatsiya qilasiz.
2-mashq yechimi¶
Domen 12, xizmat (dublyor) 7, ombor (DB integratsiya) 6, API 6, E2E 1 β jami 32. Guruhlab: tez (unit + dublyor) 19, integratsiya + API 12, E2E 1. E2E eng kam, chunki u sekin, mo'rt va xato qayerda ekanligini aniq ko'rsatmaydi; unit eng ko'p, chunki tez, izolyatsiyalangan va yiqilganda muammoni aniq joyga ko'rsatadi. Pastda tez/arzon, tepada sekin/qimmat β shuning uchun asos keng (03-bob).
3-mashq yechimi¶
tolov.yuborilgan == [] β to'lov metodi chaqirilmaganini tekshiradi. FakeTolov bu yerda spy
vazifasini bajaradi (chaqiruvlarni yuborilgan ro'yxatida qayd qiladi). Bu xulq-atvor (behavior)
tekshiruvi (07-bob): jarima nol bo'lganda kod tashqi servisni bezovta qilmasligini isbotlaydi.
Agar kodda xato bo'lib, har doim tola chaqirilsa, bu test uni ushlaydi.
4-mashq yechimi¶
| Komponent | Xato ehtimoli | Ta'sir | Reja |
|---|---|---|---|
| Ijaralar tarixini ko'rish | Past (faqat o'qish, SELECT) | Past (noto'g'ri ro'yxat β pul/ma'lumot xavfi yo'q) | 1β2 integratsiya testi (to'g'ri filtr/tartib), unit shart emas |
Izoh: bu β faqat o'qiydigan, yon ta'sirsiz so'rov. Ta'siri past (xato bo'lsa ham faqat ko'rinish noto'g'ri),
shuning uchun og'ir qoplash isrof. IjaraOmboriga foydalanuvchi_boyicha so'rov qo'shib, bir-ikki
integratsiya testi yetarli β masalan, faqat o'zining ijaralarini ko'rishini va to'g'ri tartibni
tekshirish.
5-mashq yechimi¶
Mavjud testlar uni ushlamadi, chunki qaytarilgan == muddat holatida kechikish = 0, va 0 * 1000 = 0
β natija ikkala variant uchun (<= 0 ham, < 0 ham) bir xil. Mutantni o'ldirish uchun shartni
ko'rinadigan qiladigan tekshiruv kerak emas β natija baribir bir xil. Demak bu mutant ekvivalent
mutant bo'lishi mumkin (22-bob): u xulqni umuman o'zgartirmaydi, shuning uchun hech qanday test uni
o'ldira olmaydi. To'g'ri javob: qo'shimcha test qo'shib bo'lmaydi β buni "ekvivalent mutant" deb
belgilaymiz va e'tiborsiz qoldiramiz. (Agar return 0 o'rniga return -1 bo'lganida edi, u holda
(14, 14, 0) testi uni o'ldirardi.)
6-mashq yechimi¶
π΄ Red:
def test_bekor_qilingan_qaytarib_bolmaydi(xizmat, ulanish):
kid = KitobOmbori(ulanish).qosh("Kata", 1)
ijara_id = xizmat.ijaraga_ber("aziz", kid, muddat_kuni=14)
xizmat.bekor_qil(ijara_id) # yangi metod
with pytest.raises(IjaraXatosi, match="bekor qilingan"):
xizmat.qaytar(ijara_id, qaytarilgan_kuni=14)
# -> FAILED (bekor_qil metodi va guard hali yo'q)
π’ Green β eng kichik kod: ijaraga bekor_qilingan maydoni, bekor_qil metodi, va qaytar
boshida guard:
def bekor_qil(self, ijara_id):
self.ijaralar.bekor_deb_belgila(ijara_id)
# qaytar() boshida:
if ijara["bekor_qilingan"]:
raise IjaraXatosi("Ijara bekor qilingan")
π΅ Refactor β qaytar boshidagi uchta guard ("topilmadi", "allaqachon qaytarilgan", "bekor
qilingan") bir uslubda; istasangiz ularni _tekshir(ijara) yordamchi funksiyasiga chiqarasiz. Barcha
eski testlar yashil qolishini tekshiring β regressiya yo'q.
7-mashq yechimi¶
def test_e2e_limit_va_qaytarish(api, ulanish):
ids = [KitobOmbori(ulanish).qosh(f"K{i}", 1) for i in range(4)]
for kid in ids[:3]: # 3 ta ijara β ok
s, _ = api.post_ijara({"foydalanuvchi": "ali", "kitob_id": kid, "muddat_kuni": 14})
assert s == 201
s, javob = api.post_ijara( # 4-chi β limit
{"foydalanuvchi": "ali", "kitob_id": ids[3], "muddat_kuni": 14})
assert s == 409 and "limit" in javob["xato"]
# bittasini qaytaramiz -> joy bo'shaydi
i1 = api.post_ijara # (mavjud ijara_id ni saqlab oling)
# ... bitta ijarani qaytargach, 4-chi kitobni qayta urinib 201 olinadi
# -> ijaralar limiti va bo'shashi to'liq oqimda tekshiriladi
Ishtirok etadigan qatlamlar: API (status kodlar), xizmat (ijara_mumkinmi limit qoidasi),
domen (MAKS_KITOB), ombor (faol ijaralar sonini DB'dan sanash). Demak bu β to'rt qatlamni
birga sinaydigan haqiqiy E2E.
8-mashq yechimi¶
Teskari piramida ("muzqaymoq" β ko'p E2E, oz unit) uchta muammo: (1) Tezlik β E2E testlar sekin (brauzer, tarmoq, butun stek); 20 ta E2E to'plamni daqiqalarga cho'zadi, tez fikr-mulohaza yo'qoladi (27-bob). (2) Ishonch/barqarorlik β E2E ko'p tashqi qismga tegadi, shuning uchun flaky bo'ladi: goh o'tadi, goh yiqiladi (24-bob); jamoa "yana flaky" deb yiqilishlarga e'tibor bermay qo'yadi. (3) Diagnostika β E2E yiqilsa, qayer buzilganini aytmaydi (domen? DB? API?); 1 ta unit yiqilsa, aniq funksiyani ko'rsatadi. Bu "muzqaymoq" anti-naqsh, chunki og'irlik notog'ri joyda β keng asos (tez, aniq unit) o'rniga og'ir uch (sekin, noaniq E2E) bor (03-bob).
π README Β· β¬ οΈ Oldingi: 29 β Eski (legacy) kodni testlash