02 β Birinchi testingiz: AAA va test anatomiyasi¶
π README Β· β¬ οΈ Oldingi: 01 β Nega test yozamiz? Β· Keyingi: 03 β Test turlari va test piramidasi β‘οΈ
Bu bobda: terminalingizda birinchi avtomat testni yozasiz va ishga tushirasiz. Test runner (pytest) nima ekanini,
assertqanday ishlashini, va har bir testni o'qishga oson qiladigan AAA naqshi (Arrange-Act-Assert) bilan tanishasiz. Yaxshi test nomi qanday bo'lishini ko'rasiz va pytest test fayllaringizni qanday avtomat topishini bilib olasiz.Halollik / Eslatma: misollar Python +
pytestustida, lekin AAA, test nomlash va "bitta test = bitta xulq" tamoyillari har qanday tilda bir xil. Bu bobda eng oddiy testlardan boshlaymiz; fixture, parametrize va mock kabi kuchli vositalar keyingi boblarga qoldiriladi. Bu bobdagi barcha kod va pytest chiqishlari haqiqatan ishga tushirib tekshirilgan.
Test runner nima va nega kerak¶
Tasavvur qiling, siz oshxonada ovqat pishirdingiz va har gal mehmon kelganda uni qo'lda tatib ko'rasiz: tuz yetarlimi, issiqmi. Bu β qo'lda testlash. Endi tasavvur qiling, sizning o'rningizga bir yordamchi bor: u har gal ovqat tayyor bo'lishi bilan o'zi tatib, "tuz joyida, harorat joyida" deb hisobot beradi β bir soniyada, charchamasdan, har safar aynan bir xil. Mana shu yordamchi β test runner.
Test runner β bu sizning testlaringizni topib, ishga tushirib, natijani hisobot qiladigan dastur. Python dunyosida eng mashhuri β pytest. Siz faqat "nimani tekshirish kerak"ni yozasiz; topish, chaqirish, natijani yashil yoki qizil qilib ko'rsatish β runner'ning ishi.
Nega runner kerak? Chunki bitta funksiyani bir marta qo'lda tekshirish oson, ammo loyiha o'sgani sayin yuzlab tekshiruv paydo bo'ladi. Ularning hammasini har o'zgarishdan keyin qo'lda qaytarish mumkin emas. Runner buni bir buyruq bilan, soniyalarda bajaradi.
Til-ko'prik: JavaScript'da runner β
JestyokiVitest; PHP'da βPHPUnityokiPest; Java'da βJUnit; Go'da β o'rnatilgantestingpaketi. Nomi boshqacha, vazifasi bir xil: testlarni topadi, ishga tushiradi, hisobot beradi.
pytest'ni o'rnatish va ishga tushirish¶
pytest oddiy paket. Terminalda:
O'rnatilganini tekshirish:
Testlarni ishga tushirish β joriy papkada shunchaki:
Batafsilroq, har test nomini ko'rsatib ishga tushirish:
-v (verbose, ya'ni "gapdor") har bir testni alohida qatorda PASSED yoki FAILED bilan ko'rsatadi.
Tezda ko'rasiz, bu farqning ahamiyati katta.
Eng birinchi testingiz¶
Kichik bir funksiyadan boshlaylik: ikki sonni qo'shadigan qoshish. Avval sinaladigan kodni
alohida faylga yozamiz.
Fayl: kalkulyator.py (bu β biz testlayotgan haqiqiy kod):
Endi test kodini alohida faylga yozamiz. Bu yerda eng muhim konvensiya bor: pytest test
fayllarini nomidan tanib oladi β fayl nomi test_ bilan boshlanishi (yoki _test
bilan tugashi) kerak, va ichidagi test funksiyalari ham test_ bilan boshlanishi kerak.
Fayl: test_kalkulyator.py (bu β testlar):
from kalkulyator import qoshish
def test_ikki_musbat_son_qoshilganda_yigindi_qaytadi():
natija = qoshish(2, 3)
assert natija == 5
Ikki fayl bir papkada yonma-yon turadi:
Endi terminalda papkada turib pytest deymiz:
pytest
# ->
# ======================== test session starts ========================
# collected 1 item
#
# test_kalkulyator.py . [100%]
#
# ========================= 1 passed in 0.53s =========================
Tabriklaymiz β siz birinchi avtomat testingizni yozib, ishga tushirdingiz. O'sha . (nuqta)
"bitta test o'tdi" degani. 1 passed β hammasi yashil.
pytest -v bilan ko'rsa, test nomi to'liq ko'rinadi:
pytest -v
# ->
# test_kalkulyator.py::test_ikki_musbat_son_qoshilganda_yigindi_qaytadi PASSED [100%]
# ========================= 1 passed in 0.52s =========================
Eslatma: kodni va testni nega ikki faylga ajratamiz? Chunki
kalkulyator.pyβ ishlab chiqarishga (production) ketadigan haqiqiy kod,test_kalkulyator.pyesa faqat tekshirish uchun. Ularni ajratish kodingizni toza saqlaydi va testlarni alohida ishga tushirish, alohida tashlab yuborishni osonlashtiradi.
assert qanday ishlaydi¶
assert β Python'ning o'rnatilgan kalit so'zi. U juda oddiy: assert SHART deyilsa, agar
SHART rost bo'lsa β hech narsa bo'lmaydi, kod davom etadi. Agar yolg'on bo'lsa β
AssertionError istisnosi (exception) tashlanadi va test yiqiladi.
assert 5 == 5 # rost -> hech narsa bo'lmaydi
assert 4 == 5 # yolg'on -> AssertionError, test yiqiladi
Test runner mana shu mexanizmga tayanadi: test funksiyasi oxirigacha istisno tashlamasdan
yetib borsa β PASSED; biror assert yiqilsa (yoki kutilmagan istisno chiqsa) β FAILED.
pytest'ning aqlli xabari (assertion introspection)¶
Oddiy assert yiqilganda Python faqat "AssertionError" deydi β qaysi qiymatlar mos kelmaganini
aytmaydi. pytest esa assertion introspection (tekshiruvni o'rganib chiqish) qiladi: yiqilgan
assertning ikki tomonidagi haqiqiy qiymatlarni o'zi hisoblab, sizga ko'rsatadi. Bu β
xatoni topishni tezlashtiradigan eng yoqimli xususiyatlardan biri.
Ataylab yiqiladigan test yozamiz: qoshish(2, 2) natijasini xato qilib 5 deb kutamiz.
from kalkulyator import qoshish
def test_qoshish_xato_kutilsa_yiqiladi():
natija = qoshish(2, 2)
assert natija == 5
Ishga tushiramiz:
pytest test_yiqiladi.py
# ->
# =============================== FAILURES ===============================
# ___________________ test_qoshish_xato_kutilsa_yiqiladi _________________
#
# def test_qoshish_xato_kutilsa_yiqiladi():
# natija = qoshish(2, 2)
# > assert natija == 5
# E assert 4 == 5
#
# test_yiqiladi.py:6: AssertionError
# ========================== 1 failed in 0.68s ==========================
Diqqat qiling: pytest aynan assert 4 == 5 deb yozdi. Ya'ni natija aslida 4 bo'lgan, biz
esa 5 kutgan ekanmiz β bu yiqilishning sababi darrov ko'rinib turibdi. Kodga kirib
qidirishga hojat yo'q.
Endi tuzatamiz β kutilgan qiymatni to'g'rilaymiz (2 + 2 = 4):
def test_qoshish_tuzatildi():
natija = qoshish(2, 2)
assert natija == 4
# ->
# ========================== 1 passed in 0.53s ==========================
Yashil. Mana shu sikl β yozish, ishga tushirish, qizilni ko'rish, tuzatish, yashilni ko'rish β testlash bilan ishlashning kundalik ritmi.
AAA naqsh: Arrange β Act β Assert¶
Eng yaxshi testlar bir xil shaklga ega bo'ladi. Bu shakl β AAA naqsh: har test uchta aniq bosqichdan iborat:
- Arrange (tayyorla) β testga kerakli obyekt, ma'lumot va boshlang'ich holatni tayyorlaymiz.
- Act (bajar) β tekshirilayotgan bitta amalni ishga tushiramiz.
- Assert (tekshir) β natija kutilgan qiymatga mosligini tasdiqlaymiz.
Buni real misolda ko'raylik. Bizda kichik savat (shopping cart) klassi bor.
Fayl: savat.py (sinaladigan kod):
class Savat:
def __init__(self):
self.mahsulotlar = []
def qoshish(self, nom, narx):
self.mahsulotlar.append((nom, narx))
def jami(self):
return sum(narx for _, narx in self.mahsulotlar)
Fayl: test_savat.py β har bosqichni izoh bilan ajratamiz:
from savat import Savat
def test_ikki_mahsulot_qoshilganda_jami_narxlar_yigindisi_boladi():
# Arrange β savatni tayyorlaymiz
savat = Savat()
savat.qoshish("non", 5000)
savat.qoshish("sut", 12000)
# Act β tekshirilayotgan amalni bajaramiz
jami = savat.jami()
# Assert β natijani kutilgan qiymat bilan solishtiramiz
assert jami == 17000
Ishga tushiramiz:
pytest test_savat.py -v
# ->
# test_savat.py::test_ikki_mahsulot_qoshilganda_jami_narxlar_yigindisi_boladi PASSED [ 50%]
# test_savat.py::test_bosh_savatning_jamisi_nol PASSED [100%]
# ========================== 2 passed in 0.55s ==========================
Nega AAA o'qishni osonlashtiradi¶
Ko'zingiz testni yuqoridan pastga o'qiganda darrov javob topadi: "Nima holatda? (Arrange) Nima qildik? (Act) Nimani kutamiz? (Assert)". Uch bosqich bir-biriga aralashmasa, test bir nigohda tushunarli bo'ladi.
Quyidagi anti-misolga qarang β bu test ham ishlaydi, lekin bosqichlar aralashib ketgan, o'qish qiyin:
# β Yomon: tayyorlash, amal va tekshiruv aralash
def test_savat():
savat = Savat()
savat.qoshish("non", 5000)
assert savat.jami() == 5000 # tekshiruv o'rtada
savat.qoshish("sut", 12000)
assert savat.jami() == 17000 # yana tekshiruv
# β
Yaxshi: bir holat, bir amal, bir tekshiruv (AAA aniq)
def test_ikki_mahsulot_qoshilganda_jami_narxlar_yigindisi_boladi():
savat = Savat() # Arrange
savat.qoshish("non", 5000)
savat.qoshish("sut", 12000)
jami = savat.jami() # Act
assert jami == 17000 # Assert
Trade-off: har testga
# Arrange / # Act / # Assertizohini yozish shartmi? Yo'q β kichik testlarda bo'sh qator bilan ajratish ham yetadi. Muhimi β shakl, izoh emas. Lekin boshlang'ich paytda izohlar fikrni tartibga solishga juda yordam beradi.Eslatma: ba'zan to'rtinchi bosqich qo'shiladi β Cleanup (tozalash), masalan ochilgan fayl yoki ulanishni yopish. pytest'da buni odatda fixture bajaradi (06-bobda). Hozircha uch bosqich yetarli.
Test nomlash: nom o'zi hikoya aytsin¶
Test yiqilganda siz birinchi ko'radigan narsa β uning nomi. Yaxshi nom shu zahoti "nima buzilgani"ni aytib beradi; yomon nom esa sizni kodga qarashga majbur qiladi.
Yaxshi nom uch qismdan iborat: nima testlanadi + qaysi sharoitda + qanday natija kutiladi.
Misollar:
| β Yomon nom | β Yaxshi nom |
|---|---|
test_qoshish |
test_manfiy_sonlar_qoshilganda_togri_yigindi |
test_1 |
test_bosh_savatning_jamisi_nol |
test_ishlaydimi |
test_mavjud_bolmagan_mahsulot_ochirilsa_xato_chiqadi |
test_savat |
test_ikki_mahsulot_qoshilganda_jami_yigindi_boladi |
test_qoshish yiqilsa, siz "qo'shishning qaysi holatida?" deb so'raysiz β manfiy sonlarmi,
nolmi, juda katta sonmi? Nom buni aytmaydi. test_manfiy_sonlar_qoshilganda_togri_yigindi
esa darrov aytadi: muammo manfiy sonlarni qo'shishda.
Diqqat: uzun nomdan qo'rqmang. Test nomini siz qo'lda yozmaysiz β runner uni faqat hisobotda ko'rsatadi. "Uzun, lekin aniq" har doim "qisqa, lekin sirli"dan afzal.
Bitta test = bitta xulq¶
Yaxshi testning yana bir xossasi: u bitta xulqni (one logical assertion / bitta yiqilish sababi) tekshiradi. Ya'ni test yiqilsa, faqat bitta sabab bo'lishi kerak.
# β Bir testda uch xil xulq aralash β yiqilsa, qaysi biri buzildi?
def test_savat_hammasi():
savat = Savat()
savat.qoshish("non", 5000)
assert savat.jami() == 5000
assert len(savat.mahsulotlar) == 1
assert savat.mahsulotlar[0][0] == "non"
Bu testda uchta turli da'vo bor. Birinchisi yiqilsa, pytest qolganlarini tekshirmaydi ham β demak siz to'liq manzarani ko'rmaysiz. Har xulqni alohida testga ajratish yaxshiroq:
# β
Har test bitta xulq β yiqilsa, sabab aniq
def test_mahsulot_qoshilganda_jami_oshadi():
savat = Savat()
savat.qoshish("non", 5000)
assert savat.jami() == 5000
def test_mahsulot_qoshilganda_soni_birga_oshadi():
savat = Savat()
savat.qoshish("non", 5000)
assert len(savat.mahsulotlar) == 1
Eslatma: "bitta xulq" "bitta
assertqatori" degani emas. Ba'zan bitta xulqni tasdiqlash uchun bir nechtaassertkerak bo'ladi (masalan, bir obyektning bir nechta maydoni). Muhimi β barchaassertlar bitta g'oyani tekshirsin. Bunga 04-bobda chuqurroq qaytamiz.
Test fayllari tashkili va pytest qanday topadi (discovery)¶
pytest testlarni avtomat topadi β buni "discovery" (kashf qilish) deyiladi. U qat'iy, oddiy qoidalarga amal qiladi:
| Element | Konvensiya | Misol |
|---|---|---|
| Test fayli | test_*.py yoki *_test.py |
test_savat.py |
| Test funksiyasi | test_ bilan boshlanadi |
def test_jami_togri(): ... |
| Test klassi (ixtiyoriy) | Test bilan boshlanadi, __init__ siz |
class TestSavat: ... |
| Test papkasi (odatda) | tests/ |
tests/test_savat.py |
Mana shu nomlash konvensiyasiga rioya qilsangiz β pytest fayllaringizni avtomat topadi, ro'yxatdan o'tkazadi va ishga tushiradi. Hech qaysi faylni qo'lda ro'yxatga olish kerak emas.
Kichik loyihada kod va testlar bir papkada yonma-yon tursa bo'ladi. Loyiha o'sganda esa testlarni
alohida tests/ papkasiga ajratish odatga aylangan:
proj/
βββ kalkulyator.py
βββ savat.py
βββ tests/
βββ test_kalkulyator.py
βββ test_savat.py
Endi loyiha ildizida shunchaki pytest desangiz, u butun daraxtni aylanib chiqib, barcha
test_*.py fayllarni topadi. Bizning sinov loyihamizda uchta test fayli bor edi, va bir
pytest buyrug'i hammasini topib ishga tushirdi:
pytest
# ->
# collected 5 items
#
# test_kalkulyator.py .. [ 40%]
# test_savat.py .. [ 80%]
# test_yiqiladi.py F [100%]
#
# ===================== 1 failed, 4 passed in 0.68s =====================
. β o'tgan test, F β yiqilgan test. Bir nigohda: 4 ta yashil, 1 ta qizil.
Eslatma:
test_prefiksi shunchaki kelishuv emas β pytest aynan shunga qarab qaror qiladi. Agar test funksiyangiznitekshir_jamideb nomlasangiz, pytest uni umuman ko'rmaydi va jimgina o'tkazib yuboradi. "Testim ishlamayapti" muammosining eng keng tarqalgan sababi β noto'g'ri nom.
Til-ko'prik: AAA universaldir¶
AAA naqshi Python'ga xos emas β u barcha tillarda bir xil. JavaScript'da (Jest/Vitest) xuddi shu test shunday ko'rinadi:
// JavaScript β Jest/Vitest
test('ikki mahsulot qoshilganda jami yigindi boladi', () => {
const savat = new Savat(); // Arrange
savat.qoshish('non', 5000);
savat.qoshish('sut', 12000);
const jami = savat.jami(); // Act
expect(jami).toBe(17000); // Assert
});
Farq faqat sintaksisda: Python'da assert jami == 17000, JS'da expect(jami).toBe(17000),
PHP'da (PHPUnit) $this->assertEquals(17000, $jami). Fikr β tayyorla, bajar, tekshir β
bir xil. Bir tilda AAA'ni o'rgansangiz, boshqasiga o'tish β faqat sintaksisni almashtirish.
Asosiy g'oyalar (bobni qisqacha)¶
- Test runner (pytest) testlarni topadi, ishga tushiradi va hisobot beradi β qo'lda tekshirishni avtomatlashtiradi.
assert SHARTrost bo'lsa o'tadi, yolg'on bo'lsa testni yiqitadi. Test runner shu mexanizmga tayanadi: istisnosiz tugasa PASSED, yiqilsa FAILED.- pytest assertion introspection yiqilgan tekshiruvning haqiqiy qiymatlarini ko'rsatadi
(
assert 4 == 5) β xatoni topishni tezlashtiradi. - AAA naqsh β Arrange (tayyorla) β Act (bajar) β Assert (tekshir). Har testni bir nigohda o'qishga oson qiladi.
- Yaxshi test nomi = nima + sharoit + kutilgan natija. Yiqilganda nomning o'zi sababni aytadi.
- Bitta test = bitta xulq. Yiqilsa, sabab bitta bo'lsin; bir testga ko'p xil g'oyani tiqmang.
- Discovery: pytest
test_*.pyfayllar vatest_funksiyalarni avtomat topadi. Noto'g'ri nom = test ko'rinmaydi. - AAA va nomlash til-mustaqil β JS, PHP, Java'da ham aynan shu tamoyillar.
Mashqlar¶
Oson¶
1-mashq. kopaytir(a, b) funksiyasini yozing va unga bitta test_ funksiyasi yarating.
Test nomi "nima + sharoit + natija" qoidasiga mos bo'lsin. pytest bilan ishga tushiring.
2-mashq. Quyidagi testning AAA bosqichlarini izoh bilan ajrating:
3-mashq. Ataylab yiqiladigan test yozing (assert qoshish(2, 3) == 6). pytest chiqishida
assert ... == ... qatorini toping va u nimani ko'rsatishini tushuntiring.
O'rta¶
4-mashq. Quyidagi nomlarni "yaxshi"ga aylantiring: test_login, test_2, test_narx.
Har biri uchun nima testlanayotgani + sharoit + kutilgan natijani o'ylab toping.
5-mashq. Savatga ochirish(nom) metodini qo'shing (mahsulotni nomi bo'yicha o'chiradi).
Keyin AAA bo'yicha test yozing: "mavjud mahsulot o'chirilsa, jami kamayadi".
6-mashq. Quyidagi "hammasini birga" testni uchta alohida, "bitta xulq" testiga bo'ling:
def test_savat():
s = Savat()
s.qoshish("non", 5000)
assert s.jami() == 5000
assert len(s.mahsulotlar) == 1
Qiyin¶
7-mashq. Bitta test fayl yozing, lekin bitta test funksiyasini test_ siz nomlang
(masalan tekshir_jami). pytest -v ishga tushiring va u necha testni topganini kuzating.
Nima uchun u funksiya hisobotda yo'qligini tushuntiring.
8-mashq. tests/ papka tuzilmasini yarating: ildizda savat.py, ichida tests/test_savat.py.
Ildizdan pytest ishga tushiring. Agar ModuleNotFoundError: savat chiqsa, nega yuzaga
kelishini va uni qanday hal qilish mumkinligini (masalan, ildizdan ishga tushirish yoki
__init__.py / conftest.py) muhokama qiling.
Yechimlar
1-mashq yechimi¶
amallar.py:
test_amallar.py:
from amallar import kopaytir
def test_ikki_son_kopaytirilganda_kopaytma_qaytadi():
natija = kopaytir(4, 5)
assert natija == 20
# -> 1 passed in 0.5s
2-mashq yechimi¶
def test_xx():
# Arrange
savat = Savat()
savat.qoshish("olma", 3000)
# Act
jami = savat.jami()
# Assert
assert jami == 3000
jami() chaqiruvi) va tekshiruv (assert) aniq ajraldi.
3-mashq yechimi¶
def test_qoshish_xato_yiqiladi():
assert qoshish(2, 3) == 6
# ->
# > assert qoshish(2, 3) == 6
# E assert 5 == 6
assert 5 == 6 deb ko'rsatadi: qoshish(2, 3) aslida 5 qaytargan, biz 6 kutgan
edik. Introspection ikki tomonning haqiqiy qiymatini hisoblab beradi, shuning uchun sababni
darrov ko'ramiz.
4-mashq yechimi¶
| Yomon | Yaxshi |
|---|---|
test_login |
test_togri_parol_bilan_kirilganda_sessiya_ochiladi |
test_2 |
test_bosh_royxatda_element_topilmaganda_none_qaytadi |
test_narx |
test_chegirma_qollanilganda_narx_kamayadi |
Har nom: nima (login/qidiruv/narx) + sharoit (to'g'ri parol / bo'sh ro'yxat / chegirma) + natija (sessiya ochiladi / None / narx kamayadi).
5-mashq yechimi¶
savat.pyga qo'shish:
def test_mavjud_mahsulot_ochirilganda_jami_kamayadi():
# Arrange
savat = Savat()
savat.qoshish("non", 5000)
savat.qoshish("sut", 12000)
# Act
savat.ochirish("sut")
# Assert
assert savat.jami() == 5000
# -> 1 passed
6-mashq yechimi¶
def test_mahsulot_qoshilganda_jami_narxga_teng():
s = Savat()
s.qoshish("non", 5000)
assert s.jami() == 5000
def test_mahsulot_qoshilganda_soni_bittaga_oshadi():
s = Savat()
s.qoshish("non", 5000)
assert len(s.mahsulotlar) == 1
7-mashq yechimi¶
pytest -v chiqishi: collected 0 items β pytest bu funksiyani umuman ko'rmaydi, chunki
nomi test_ bilan boshlanmaydi. Discovery qoidasi qat'iy: faqat test_ prefiksli funksiyalar
test deb hisoblanadi. Bu β "testim ishlamayapti, lekin xato ham yo'q" muammosining eng tez-tez
uchraydigan sababi.
8-mashq yechimi¶
Tuzilma:
test_savat.py ichida from savat import Savat deyilsa, pytestni proj/ ildizidan
ishga tushirsangiz odatda ishlaydi, chunki pytest ildiz papkasini import yo'liga qo'shadi
(rootdir mexanizmi). Agar ModuleNotFoundError: savat chiqsa, sababi β Python savat.pyni
qidirayotgan yo'lda topa olmayapti. Yechimlar: (1) pytestni aynan loyiha ildizidan ishga
tushirish; (2) ildizga bo'sh conftest.py qo'yish (pytest uni ildiz sifatida tan oladi va
yo'lga qo'shadi); (3) loyihani paket sifatida o'rnatish (pip install -e .). Eng oddiy
boshlang'ich yechim β ildizdan ishga tushirish yoki ildizga conftest.py qo'yish.
π README Β· β¬ οΈ Oldingi: 01 β Nega test yozamiz? Β· Keyingi: 03 β Test turlari va test piramidasi β‘οΈ