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.

🕑 15 daqiqa o‘qish 📄 926 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Tuzilish: uch qadam
  2. Nom - gap bo'lsin
  3. Testlar bir-biriga bog'liq bo'lmasin
  4. Test ichida mantiq yozmang
  5. Xulosa

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.

Har bir testning uch qismi 1. Tayyorlash Kerakli ma'lumot va obyektlarni yaratasiz baholar = [70, 80] 2. Bajarish Tekshirilayotgan amalni bajarasiz - bitta chaqiruv natija = ortacha(baholar) 3. Tekshirish Natija kutilganmi - faqat shuni tasdiqlaysiz assert natija == 75 Ikkinchi qismda bitta chaqiruv bo'lsin Agar u yerda uchta chaqiruv bo'lsa, test nimani tekshirayotgani noaniq bo'lib qoladi
Uch qism bo'sh qator bilan ajratilsa, test bir qarashda o'qiladi

Farqni ko'rish uchun bir xil narsani ikki xil yozamiz.

Avval tekshiriladigan kod - savat (xaridlar ro'yxati):

Python
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:

Python
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:

Python
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 testYaxshi test
Yiqilganda nomitest_savattest_chegirma_jamidan_ayiriladi
Nima buzilganiNoma'lum, kodni ochish kerakNomidan ko'rinadi
Nechta xato ko'rinadiBittasi - keyingilari bajarilmaydiUchalasi ham
Tuzatilgandan keyinYana ishga tushirib ko'rish kerakBoshqalari allaqachon aniq

Buni o'lchab ko'ramiz. Ikkala faylda ham bitta xato bo'lsin - masalan chegirma_bilan ni buzamiz:

Python
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)
Terminal
pytest -q --tb=no
Natija
..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:

Python
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)
Terminal
pytest -q --tb=no
Natija
.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:

Python
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 nomYaxshi nom
test_1test_bosh_savat_jamisi_nol
test_savattest_bir_xil_mahsulot_soni_qoshiladi
test_xatotest_nol_soni_xato_beradi
test_chegirmatest_yuz_foiz_chegirma_jamini_nolga_tushiradi
test_ishlayditest_narx_kasr_bolsa_butunga_yumaloqlanadi

Nomlar -v bayrog'ida qanday ko'rinishini eslang: ular hisobotning o'zi bo'ladi.

Test nomini yozishning oson usuli

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:

Python
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
Terminal
pytest -q test_boglangan.py
Natija
..                                                               [100%]
2 passed in 0.01s

Ikkalasi ham o'tdi. Endi faqat ikkinchisini ishga tushiramiz - xuddi siz bitta testni tuzatayotganda qiladigandek:

Terminal
pytest -q "test_boglangan.py::test_ikkinchi_mahsulot"
Natija
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.

Umumiy holat - beqaror testlarning bosh sababi

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:

Python
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
Terminal
pytest -q "test_boglangan.py::test_ikkinchi_mahsulot"
Natija
.                                                                [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:

Python
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
Terminal
pytest -q test_mantiq.py
Natija
.                                                                [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 uchrasaNima qilish kerak
for sikli@pytest.mark.parametrize ga aylantiring
if shartiIkkita alohida test yozing
try/exceptpytest.raises ishlating
Hisob-kitobKutilgan 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.

Kutilgan qiymatni testda hisoblamang

Yomon:

Python
assert savat.chegirma_bilan(10) == round(savat.jami() * 0.9)

Yaxshi:

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

Amaliy topshiriq
  1. savat.py va ikkala test faylini yozib, ikkalasini ishga tushiring.
  2. Ikkita qoidani birdan buzib, ikkala hisobotdagi yiqilishlar sonini sanang.
  3. test_savat nomiga qarab, nima buzilganini ayta olasizmi?
  4. Bog'langan testlarni yozib, ikkinchisini yolg'iz ishga tushiring.
  5. Ularni mustaqil qilib qayta yozing va yana tekshiring.
  6. O'z loyihangizdagi testlardan bittasini uch qismga ajrating.
  7. Loyihangizda modul darajasida yaratilgan obyekt bormi - qidirib ko'ring.
  8. Bo'sh sikl testini yozib, u nima uchun o'tayotganini tushuntiring.
  9. Quyidagi nomni yaxshilang: test_hisob2.
  10. 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 for bo'sh ro'yxat ustida aylansa, test hech narsa tekshirmasdan o'tadi.
  • for ni parametrga, if ni ikkita testga, try ni pytest.raises ga 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.

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.