5-bo‘lim
Yaxshi testning anatomiyasi
Tayyorlash-Bajarish-Tekshirish tuzilishi, test nomini gap qilib yozish, testlarning bir-biriga bog'liq bo'lmasligi va test ichida mantiq yozmaslik.
Ushbu bo‘lim mundarijasi
Ikkita test bir xil xatoni tutishi mumkin, lekin biri yiqilganda sizga hamma narsani aytadi, ikkinchisi esa yarim soatingizni oladi.
Bu bo'lim - ana shu farq haqida.
Tuzilish: uch qadam #
Yaxshi testning ichida doim uch qism bo'ladi. Ingliz tilida ular Arrange-Act-Assert, o'zbekchada esa Tayyorlash - Bajarish - Tekshirish deb aytish mumkin.
Farqni ko'rish uchun bir xil narsani ikki xil yozamiz.
Avval tekshiriladigan kod - savat (xaridlar ro'yxati):
class Savat:
def __init__(self):
self.mahsulotlar = {}
def qosh(self, nom, narx, soni=1):
if soni < 1:
raise ValueError("soni kamida 1 bo'lishi kerak")
mavjud = self.mahsulotlar.get(nom, {"narx": narx, "soni": 0})
mavjud["soni"] += soni
self.mahsulotlar[nom] = mavjud
def jami(self):
return sum(m["narx"] * m["soni"] for m in self.mahsulotlar.values())
def chegirma_bilan(self, foiz):
return round(self.jami() * (100 - foiz) / 100)
Yomon yozilgan test:
from savat import Savat
def test_savat():
s = Savat()
s.qosh("non", 5000)
assert s.jami() == 5000
s.qosh("sut", 12000, 2)
assert s.jami() == 29000
assert s.chegirma_bilan(10) == 26100
assert len(s.mahsulotlar) == 2
O'sha narsa, lekin uch qismga ajratilgan holda:
from savat import Savat
def test_bitta_mahsulot_jamiga_qoshiladi():
savat = Savat()
savat.qosh("non", 5000)
assert savat.jami() == 5000
def test_bir_nechta_mahsulot_jami_yigindisi():
savat = Savat()
savat.qosh("non", 5000)
savat.qosh("sut", 12000, 2)
assert savat.jami() == 29000
def test_chegirma_jamidan_ayiriladi():
savat = Savat()
savat.qosh("non", 5000)
savat.qosh("sut", 12000, 2)
natija = savat.chegirma_bilan(10)
assert natija == 26100
Ikkalasi ham bir xil xatolarni tutadi. Farq - yiqilganda:
| Yomon test | Yaxshi test | |
|---|---|---|
| Yiqilganda nomi | test_savat | test_chegirma_jamidan_ayiriladi |
| Nima buzilgani | Noma'lum, kodni ochish kerak | Nomidan ko'rinadi |
| Nechta xato ko'rinadi | Bittasi - keyingilari bajarilmaydi | Uchalasi ham |
| Tuzatilgandan keyin | Yana ishga tushirib ko'rish kerak | Boshqalari allaqachon aniq |
Buni o'lchab ko'ramiz. Ikkala faylda ham bitta xato bo'lsin - masalan
chegirma_bilan ni buzamiz:
class Savat:
def __init__(self):
self.mahsulotlar = {}
def qosh(self, nom, narx, soni=1):
if soni < 1:
raise ValueError("soni kamida 1 bo'lishi kerak")
mavjud = self.mahsulotlar.get(nom, {"narx": narx, "soni": 0})
mavjud["soni"] += soni
self.mahsulotlar[nom] = mavjud
def jami(self):
return sum(m["narx"] * m["soni"] for m in self.mahsulotlar.values())
def chegirma_bilan(self, foiz):
return round(self.jami() * foiz / 100)
pytest -q --tb=no
..FF [100%]
======================= short test summary info ========================
FAILED test_yaxshi.py::test_chegirma_jamidan_ayiriladi - assert 2900 ...
FAILED test_yomon.py::test_savat - assert 2900 == 26100
2 failed, 2 passed in 0.01s
Ikkala faylda ham bittadan test yiqildi. Lekin test_savat nomi sizga
hech narsa aytmaydi, test_chegirma_jamidan_ayiriladi esa xatoning
qayerda ekanini to'g'ridan-to'g'ri aytib turibdi.
Endi ikkita qoidani birdan buzamiz - jami ham, chegirma_bilan ham
noto'g'ri bo'lsin:
class Savat:
def __init__(self):
self.mahsulotlar = {}
def qosh(self, nom, narx, soni=1):
if soni < 1:
raise ValueError("soni kamida 1 bo'lishi kerak")
mavjud = self.mahsulotlar.get(nom, {"narx": narx, "soni": 0})
mavjud["soni"] += soni
self.mahsulotlar[nom] = mavjud
def jami(self):
return sum(m["narx"] for m in self.mahsulotlar.values())
def chegirma_bilan(self, foiz):
return round(self.jami() * foiz / 100)
pytest -q --tb=no
.FFF [100%]
======================= short test summary info ========================
FAILED test_yaxshi.py::test_bir_nechta_mahsulot_jami_yigindisi - asse...
FAILED test_yaxshi.py::test_chegirma_jamidan_ayiriladi - assert 1700 ...
FAILED test_yomon.py::test_savat - assert 17000 == 29000
3 failed, 1 passed in 0.01s
Mana asosiy farq. Yaxshi faylda ikkita test yiqildi - jami dagi xato
ham, chegirma_bilan dagi xato ham bitta ishga tushirishda ko'rindi.
(Uchinchi test o'tdi: bitta mahsulot uchun eski va yangi jami bir xil
natija beradi.)
Yomon faylda esa yana bitta xabar. Birinchi assert yiqilgani uchun
undan keyingi qatorlar umuman bajarilmadi va chegirmadagi xato yashirin
qoldi. Siz uni faqat birinchi xatoni tuzatib, testni qaytadan ishga
tushirganingizdan keyin ko'rasiz.
Kodni qaytaramiz:
class Savat:
def __init__(self):
self.mahsulotlar = {}
def qosh(self, nom, narx, soni=1):
if soni < 1:
raise ValueError("soni kamida 1 bo'lishi kerak")
mavjud = self.mahsulotlar.get(nom, {"narx": narx, "soni": 0})
mavjud["soni"] += soni
self.mahsulotlar[nom] = mavjud
def jami(self):
return sum(m["narx"] * m["soni"] for m in self.mahsulotlar.values())
def chegirma_bilan(self, foiz):
return round(self.jami() * (100 - foiz) / 100)
Nom - gap bo'lsin #
Test nomi hisobotda o'qiladi, shuning uchun u jumla bo'lishi kerak.
Foydali shakl: test_<nima>_<qanday sharoitda>_<qanday natija>.
| Yomon nom | Yaxshi nom |
|---|---|
test_1 | test_bosh_savat_jamisi_nol |
test_savat | test_bir_xil_mahsulot_soni_qoshiladi |
test_xato | test_nol_soni_xato_beradi |
test_chegirma | test_yuz_foiz_chegirma_jamini_nolga_tushiradi |
test_ishlaydi | test_narx_kasr_bolsa_butunga_yumaloqlanadi |
Nomlar -v bayrog'ida qanday ko'rinishini eslang: ular hisobotning
o'zi bo'ladi.
Testni yozishdan oldin, yiqilganda ko'rmoqchi bo'lgan xabarni
yozing. Masalan: "nol soni xato beradi". Endi shu jumlani pastki
chiziqlar bilan bog'lab, oldiga test_ qo'ying - nom tayyor.
Agar jumlani yozolmasangiz - demak siz nimani tekshirayotganingizni aniq bilmayapsiz. Bu testni yozishdan oldin to'xtash uchun yaxshi signal.
Testlar bir-biriga bog'liq bo'lmasin #
Bu eng ko'p uchraydigan yashirin xato. Quyidagi ikkita test birga ishga tushirilsa o'tadi:
from savat import Savat
savat = Savat() # modul darajasida - hamma test uchun bitta!
def test_birinchi_mahsulot():
savat.qosh("non", 5000)
assert len(savat.mahsulotlar) == 1
def test_ikkinchi_mahsulot():
savat.qosh("sut", 12000)
assert len(savat.mahsulotlar) == 2
pytest -q test_boglangan.py
.. [100%]
2 passed in 0.01s
Ikkalasi ham o'tdi. Endi faqat ikkinchisini ishga tushiramiz - xuddi siz bitta testni tuzatayotganda qiladigandek:
pytest -q "test_boglangan.py::test_ikkinchi_mahsulot"
F [100%]
=============================== FAILURES ===============================
________________________ test_ikkinchi_mahsulot ________________________
def test_ikkinchi_mahsulot():
savat.qosh("sut", 12000)
> assert len(savat.mahsulotlar) == 2
E AssertionError: assert 1 == 2
E + where 1 = len({'sut': {'narx': 12000, 'soni': 1}})
E + where {'sut': {'narx': 12000, 'soni': 1}} = <savat.Savat object at 0x7c4530abe660>.mahsulotlar
test_boglangan.py:13: AssertionError
======================= short test summary info ========================
FAILED test_boglangan.py::test_ikkinchi_mahsulot - AssertionError: as...
1 failed in 0.01s
Test yiqildi - garchi kodda hech narsa o'zgarmagan bo'lsa ham. Sabab: ikkinchi test birinchisining qoldirgan holatiga tayangan edi.
Modul darajasidagi o'zgaruvchi, global ro'yxat, bir marta ochilgan fayl, sinf atributi - bularning hammasi testlar orasida oqib o'tadi.
Oqibatlari:
- test yolg'iz ishga tushirilganda yiqiladi;
- testlar tartibi o'zgarsa yiqiladi;
- parallel ishga tushirilganda tasodifan yiqiladi.
Qoida: har bir test o'z ma'lumotini o'zi yaratsin. Takrorlanishdan qo'rqmang - keyingi bo'limda buni fixture bilan chiroyli qilamiz.
To'g'ri yozilishi - har test o'z savatini yaratadi:
from savat import Savat
def test_birinchi_mahsulot():
savat = Savat()
savat.qosh("non", 5000)
assert len(savat.mahsulotlar) == 1
def test_ikkinchi_mahsulot():
savat = Savat()
savat.qosh("non", 5000)
savat.qosh("sut", 12000)
assert len(savat.mahsulotlar) == 2
pytest -q "test_boglangan.py::test_ikkinchi_mahsulot"
. [100%]
1 passed in 0.00s
Endi test yolg'iz ham ishlaydi.
Test ichida mantiq yozmang #
Testda if, for yoki try paydo bo'lsa - ehtiyot bo'ling. Mana
nima uchun:
from savat import Savat
def test_hamma_mahsulot_tekshiriladi():
savat = Savat()
mahsulotlar = [] # ro'yxat bo'sh qolib ketgan
for nom, narx in mahsulotlar:
savat.qosh(nom, narx)
assert savat.jami() > 0
pytest -q test_mantiq.py
. [100%]
1 passed in 0.00s
Test o'tdi, lekin ichidagi birorta assert bajarilmadi. Sikl bo'sh
ro'yxat ustida aylandi va hech narsa tekshirilmadi. Bunday test yashil
rang beradi, lekin hech qanday himoya bermaydi.
Yechim - siklni parametrga aylantirish; bu keyingi bo'limning mavzusi.
| Testda uchrasa | Nima qilish kerak |
|---|---|
for sikli | @pytest.mark.parametrize ga aylantiring |
if sharti | Ikkita alohida test yozing |
try/except | pytest.raises ishlating |
| Hisob-kitob | Kutilgan natijani qo'lda yozing |
Oxirgi qatorga alohida e'tibor: agar test ichida kutilgan natijani hisoblasangiz, siz kodning mantig'ini takrorlaysiz. Kodda xato bo'lsa, testda ham xuddi shu xato bo'ladi va ikkalasi kelishib qoladi.
Yomon:
assert savat.chegirma_bilan(10) == round(savat.jami() * 0.9)
Yaxshi:
assert savat.chegirma_bilan(10) == 26100
Birinchisi kod bilan bir xil formulani takrorlaydi - formulada xato bo'lsa, test uni hech qachon tutmaydi. Ikkinchisi esa mustaqil dalil: 26100 sonini siz qo'lda hisoblab yozgansiz.
savat.pyva ikkala test faylini yozib, ikkalasini ishga tushiring.- Ikkita qoidani birdan buzib, ikkala hisobotdagi yiqilishlar sonini sanang.
test_savatnomiga qarab, nima buzilganini ayta olasizmi?- Bog'langan testlarni yozib, ikkinchisini yolg'iz ishga tushiring.
- Ularni mustaqil qilib qayta yozing va yana tekshiring.
- O'z loyihangizdagi testlardan bittasini uch qismga ajrating.
- Loyihangizda modul darajasida yaratilgan obyekt bormi - qidirib ko'ring.
- Bo'sh sikl testini yozib, u nima uchun o'tayotganini tushuntiring.
- Quyidagi nomni yaxshilang:
test_hisob2. - O'zingiz yozgan oxirgi beshta test nomini gapga aylantiring.
Xulosa #
- Har bir testda uch qism bo'ladi: Tayyorlash - Bajarish - Tekshirish; ularni bo'sh qator bilan ajrating.
- "Bajarish" qismida bitta chaqiruv bo'lsin - aks holda test nimani tekshirayotgani noaniq.
- Bitta katta test bitta xatoni ko'rsatadi; uchta kichik test uchalasini bir vaqtda ko'rsatadi.
- Test nomi jumla bo'lishi kerak:
test_<nima>_<sharoit>_<natija>. - Nomni yozishning oson yo'li - yiqilganda ko'rmoqchi bo'lgan xabarni avval yozish.
- Jumlani yozolmaslik - testni yozishdan oldin to'xtash uchun signal.
- Testlar bir-biriga bog'liq bo'lmasligi kerak: har biri o'z ma'lumotini o'zi yaratsin.
- Modul darajasidagi o'zgaruvchi eng ko'p uchraydigan bog'liqlik manbai.
- Bog'liq test yolg'iz ishga tushirilganda, tartib o'zgarganda yoki parallel ishlaganda yiqiladi.
- Testda takrorlanishdan qo'rqmang - o'qilishi aniqlikdan muhimroq emas.
- Test ichidagi
forbo'sh ro'yxat ustida aylansa, test hech narsa tekshirmasdan o'tadi. forni parametrga,ifni ikkita testga,trynipytest.raisesga aylantiring.- Kutilgan natijani testda hisoblamang - qo'lda yozing, aks holda test kodning xatosini takrorlaydi.
Keyingi bo'limda sikllardan qutulamiz: bitta test bilan o'nta holatni tekshirishni o'rganamiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.