8-bo‘lim
Test dublyorlari - stub, mock va fake
Tashqi bog'liqlikni almashtirish usullari, beshta dublyor turi, monkeypatch va unittest.mock, patch qayerga qo'yilishi va mock qachon zarar keltiradi.
Ushbu bo‘lim mundarijasi
Kodingiz SMS yuborsa, to'lovni o'tkazsa yoki tashqi xizmatga murojaat qilsa - testda buni haqiqatda bajarib bo'lmaydi. Har ishga tushirishda mijozga SMS ketishini hech kim xohlamaydi.
Yechim - o'sha qismni dublyor bilan almashtirish.
Muammo #
Qarzdorlarga ogohlantirish yuboradigan kod:
class SmsXizmati:
"""Haqiqiy xizmat - pul turadi va internet talab qiladi."""
def yubor(self, raqam, matn):
raise RuntimeError("haqiqiy SMS yuborildi - testda bu bo'lmasligi kerak")
def ogohlantir(yuboruvchi, mijoz, qarz):
if qarz <= 0:
return False
matn = f"Hurmatli {mijoz['ism']}, sizda {qarz} so'm qarz bor."
yuboruvchi.yubor(mijoz["telefon"], matn)
return True
E'tibor bering: ogohlantir yuboruvchini o'zi yaratmaydi, uni
tashqaridan oladi. Bu kichkina qaror testlashni butunlay o'zgartiradi -
biz u yerga istalgan narsani bera olamiz.
Beshta dublyor turi #
Ularning nomlari Gerard Meszaros ning kitobidan kelib chiqqan va bugun barcha tillarda ishlatiladi:
| Tur | Nima qiladi | Qachon kerak |
|---|---|---|
| Dummy | Hech narsa qilmaydi, faqat joyni to'ldiradi | Argument talab qilinadi, lekin ishlatilmaydi |
| Stub | Oldindan belgilangan javob qaytaradi | Kodga ma'lum bir javob kerak |
| Spy | Javob beradi va chaqiruvlarni yozib boradi | Chaqirilganini tekshirish kerak |
| Mock | Chaqiruvlarni oldindan kutadi va tekshiradi | Muloqotning o'zi muhim |
| Fake | Soddalashtirilgan, lekin ishlaydigan nusxa | Haqiqiy narsa juda sekin |
Stub va spy - qo'lda yozilgani #
Eng oddiy dublyorlar uchun hech qanday kutubxona kerak emas:
from xabar import ogohlantir
class SoxtaYuboruvchi:
"""Spy: yuborilgan xabarlarni yozib boradi."""
def __init__(self):
self.yuborilganlar = []
def yubor(self, raqam, matn):
self.yuborilganlar.append((raqam, matn))
def test_qarzdorga_xabar_ketadi():
yuboruvchi = SoxtaYuboruvchi()
mijoz = {"ism": "Aziza", "telefon": "+998901234567"}
natija = ogohlantir(yuboruvchi, mijoz, 50_000)
assert natija is True
assert len(yuboruvchi.yuborilganlar) == 1
raqam, matn = yuboruvchi.yuborilganlar[0]
assert raqam == "+998901234567"
assert "50000 so'm" in matn
def test_qarzsiz_mijozga_xabar_ketmaydi():
yuboruvchi = SoxtaYuboruvchi()
mijoz = {"ism": "Bekzod", "telefon": "+998901234568"}
natija = ogohlantir(yuboruvchi, mijoz, 0)
assert natija is False
assert yuboruvchi.yuborilganlar == []
pytest -q test_qolda.py
.. [100%]
2 passed in 0.01s
O'n qatorlik sinf - va biz haqiqiy SMS yubormasdan butun mantiqni tekshirdik. Ko'p hollarda bundan ortig'i kerak ham emas.
unittest.mock - tayyor dublyor #
Python ning standart kutubxonasida tayyor dublyor bor. U har qanday metodni qabul qiladi va chaqiruvlarni o'zi yozib boradi:
from unittest.mock import Mock
from xabar import ogohlantir
def test_mock_bilan():
yuboruvchi = Mock()
mijoz = {"ism": "Aziza", "telefon": "+998901234567"}
ogohlantir(yuboruvchi, mijoz, 50_000)
yuboruvchi.yubor.assert_called_once_with(
"+998901234567",
"Hurmatli Aziza, sizda 50000 so'm qarz bor.",
)
def test_mock_chaqirilmaganini_tekshirish():
yuboruvchi = Mock()
mijoz = {"ism": "Bekzod", "telefon": "+998901234568"}
ogohlantir(yuboruvchi, mijoz, 0)
yuboruvchi.yubor.assert_not_called()
def test_mock_javob_qaytarsin():
yuboruvchi = Mock()
yuboruvchi.yubor.return_value = "yuborildi"
javob = yuboruvchi.yubor("+998901234567", "salom")
assert javob == "yuborildi"
assert yuboruvchi.yubor.call_count == 1
pytest -q test_mock.py
... [100%]
3 passed in 0.04s
Eng kerakli metodlar:
| Metod | Nimani tekshiradi |
|---|---|
assert_called_once_with(...) | Aynan bir marta, aynan shu argumentlar bilan |
assert_called_with(...) | Oxirgi chaqiruv shu argumentlar bilan |
assert_not_called() | Umuman chaqirilmaganini |
call_count | Necha marta chaqirilganini |
call_args_list | Barcha chaqiruvlar ro'yxatini |
return_value | Dublyor nima qaytarishini belgilaydi |
side_effect | Chaqiruvda istisno tashlashni yoki har safar boshqa javobni |
Mock() obyektida istalgan metodni chaqirsangiz - u ishlaydi. Nomida
xato bo'lsa ham:
yuboruvchi.yubormoq("raqam", "matn") # metod nomi xato - lekin ishlaydi
yuboruvchi.yubor.assert_called_once() # yiqiladi, chunki yubor chaqirilmagan
Shuning uchun haqiqiy sinfga bog'lab qo'yish yaxshiroq:
from unittest.mock import create_autospec
from xabar import SmsXizmati
yuboruvchi = create_autospec(SmsXizmati)
yuboruvchi.yubormoq(...) # endi AttributeError beradi
create_autospec dublyorni haqiqiy sinfning shakliga moslaydi -
mavjud bo'lmagan metod ham, noto'g'ri argument soni ham xato beradi.
monkeypatch - modul ichidagi narsani almashtirish #
Yuqoridagi misolda yuboruvchi tashqaridan berilgan edi. Har doim ham shunday bo'lmaydi - ba'zan kod funksiyani to'g'ridan-to'g'ri chaqiradi:
def valyuta_kursi():
# Haqiqiy kodda shu yerda tashqi xizmatga so'rov ketadi:
# javob = urllib.request.urlopen("https://misol.uz/kurs", timeout=5)
# return float(javob.read())
# Darslikda uning chaqirilgani darhol ko'rinishi uchun xato tashlaymiz.
raise RuntimeError("haqiqiy so'rov ketdi - testda bu bo'lmasligi kerak")
def dollarda(summa_som):
return round(summa_som / valyuta_kursi(), 2)
monkeypatch fixture'i valyuta_kursi ni test davomida almashtiradi:
import kurs
def test_dollarga_aylantirish(monkeypatch):
monkeypatch.setattr(kurs, "valyuta_kursi", lambda: 12_500.0)
natija = kurs.dollarda(125_000)
assert natija == 10.0
def test_boshqa_kurs_bilan(monkeypatch):
monkeypatch.setattr(kurs, "valyuta_kursi", lambda: 12_000.0)
assert kurs.dollarda(120_000) == 10.0
pytest -q test_monkeypatch.py
.. [100%]
2 passed in 0.02s
monkeypatch ning eng yaxshi tomoni - test tugagach o'zi qaytaradi.
Qo'lda almashtirsangiz, uni qaytarishni unutasiz va keyingi testlar
buziladi.
| Metod | Nima qiladi |
|---|---|
setattr(obyekt, "nom", yangi) | Atribut yoki funksiyani almashtiradi |
delattr(obyekt, "nom") | Atributni vaqtincha o'chiradi |
setenv("NOM", "qiymat") | Muhit o'zgaruvchisini o'rnatadi |
delenv("NOM") | Muhit o'zgaruvchisini o'chiradi |
chdir(yol) | Joriy papkani almashtiradi |
syspath_prepend(yol) | Import yo'liga qo'shadi |
Eng ko'p uchraydigan xato: patch qayerga qo'yiladi #
Endi butun bo'limning eng muhim qismiga o'tamiz. Faraz qilaylik, kod funksiyani import qilib olgan:
from kurs import valyuta_kursi
def dollar_hisoboti(summa_som):
return f"{summa_som} so'm = {round(summa_som / valyuta_kursi(), 2)} dollar"
Tabiiy ko'ringan test:
import kurs
from hisobot import dollar_hisoboti
def test_hisobot(monkeypatch):
monkeypatch.setattr(kurs, "valyuta_kursi", lambda: 12_500.0)
natija = dollar_hisoboti(125_000)
assert natija == "125000 so'm = 10.0 dollar"
pytest -q --tb=short test_notogri_patch.py
F [100%]
=============================== FAILURES ===============================
_____________________________ test_hisobot _____________________________
test_notogri_patch.py:8: in test_hisobot
natija = dollar_hisoboti(125_000)
^^^^^^^^^^^^^^^^^^^^^^^^
hisobot.py:5: in dollar_hisoboti
return f"{summa_som} so'm = {round(summa_som / valyuta_kursi(), 2)} dollar"
^^^^^^^^^^^^^^^
kurs.py:6: in valyuta_kursi
raise RuntimeError("haqiqiy so'rov ketdi - testda bu bo'lmasligi kerak")
E RuntimeError: haqiqiy so'rov ketdi - testda bu bo'lmasligi kerak
======================= short test summary info ========================
FAILED test_notogri_patch.py::test_hisobot - RuntimeError: haqiqiy so...
1 failed in 0.01s
Almashtirish ishlamadi - haqiqiy funksiya chaqirildi.
Sabab: hisobot.py dagi from kurs import valyuta_kursi qatori import
paytida funksiyaga havolani o'z moduliga ko'chirib olgan. Biz kurs
modulidagi nomni almashtirdik, lekin hisobot modulidagi nusxa eski
funksiyaga ishora qilib turibdi.
To'g'ri yechim - ishlatilgan joyda almashtirish:
import hisobot
def test_hisobot(monkeypatch):
monkeypatch.setattr(hisobot, "valyuta_kursi", lambda: 12_500.0)
natija = hisobot.dollar_hisoboti(125_000)
assert natija == "125000 so'm = 10.0 dollar"
pytest -q test_togri_patch.py
. [100%]
1 passed in 0.02s
Bu xato shunchalik ko'p uchraydiki, uning rasmiy nomi bor: *"Where to patch"*. Yodda saqlash uchun bitta jumla:
Funksiya qaysi modulda chaqirilsa, o'sha moduldagi nomni almashtiring.
Ikkita import uslubi orasidagi farq ham shu yerda ko'rinadi:
| Import uslubi | Patch qayerga |
|---|---|
from kurs import valyuta_kursi | hisobot.valyuta_kursi |
import kurs + kurs.valyuta_kursi() | kurs.valyuta_kursi |
Ikkinchi uslub testlashni osonlashtiradi - shuning uchun ko'p jamoalar uni afzal ko'radi.
Fake - ishlaydigan soddalashtirilgan nusxa #
Ba'zan dublyor haqiqatda ishlashi kerak. Masalan omborni tekshirayotgan testlarga har safar SQL yozib o'tirish og'ir:
class Ombor:
"""Interfeys - haqiqiy amalga oshirish bazaga yozadi."""
def saqla(self, mijoz):
raise NotImplementedError
def top(self, mijoz_id):
raise NotImplementedError
class XotiradagiOmbor(Ombor):
"""Fake: xuddi shunday ishlaydi, lekin xotirada."""
def __init__(self):
self.yozuvlar = {}
def saqla(self, mijoz):
self.yozuvlar[mijoz["id"]] = mijoz
def top(self, mijoz_id):
return self.yozuvlar.get(mijoz_id)
def royxatdan_otkaz(ombor, mijoz):
if ombor.top(mijoz["id"]) is not None:
raise ValueError("bunday mijoz allaqachon bor")
ombor.saqla(mijoz)
return mijoz["id"]
import pytest
from ombor import XotiradagiOmbor, royxatdan_otkaz
def test_yangi_mijoz_saqlanadi():
ombor = XotiradagiOmbor()
mijoz_id = royxatdan_otkaz(ombor, {"id": 1, "ism": "Aziza"})
assert mijoz_id == 1
assert ombor.top(1)["ism"] == "Aziza"
def test_takroriy_mijoz_xato_beradi():
ombor = XotiradagiOmbor()
royxatdan_otkaz(ombor, {"id": 1, "ism": "Aziza"})
with pytest.raises(ValueError, match="allaqachon"):
royxatdan_otkaz(ombor, {"id": 1, "ism": "Aziza"})
pytest -q test_fake.py
.. [100%]
2 passed in 0.01s
Fake mock dan ustun tomoni: u xatti-harakatni ifodalaydi. Mock bilan yozilgan testda "saqla chaqirildi" deyilardi; fake bilan esa "saqlangandan keyin topish mumkin" deyiladi - bu ancha kuchli dalil.
Dublyor qachon zarar keltiradi #
| Dublyor qo'ying | Dublyor qo'ymang |
|---|---|
| Tarmoq so'rovlariga | O'z kodingizning ichki funksiyalariga |
| To'lov va SMS xizmatlariga | Oddiy hisob-kitoblarga |
| Joriy vaqt va tasodifga | Ma'lumotlar tuzilmalariga |
| Juda sekin amallarga | Tez ishlaydigan narsalarga |
| Chaqirib bo'lmaydigan xatolarga | Faqat "toza ajratish" uchun |
Agar bitta testni yozish uchun beshta mock kerak bo'lsa, muammo testda emas - kodda. Bu funksiya juda ko'p narsaga bog'langan degani.
Odatda yechim: bog'liqliklarni argument sifatida qabul qilish (yuqoridagi
ogohlantir kabi) yoki hisob-kitobni tashqi dunyodan ajratish.
Shu sababli tajribali dasturchilar aytadi: *testni yozish qiyin bo'lsa, bu kod haqida xabar*. Test yomon emas - u faqat xabarchi.
xabar.pyni yozib, qo'lda spy sinfi bilan ikkita testni ishga tushiring.- Spy ga uchinchi test qo'shing: ikki mijozga xabar ketishini tekshiring.
- Xuddi shu testni
Mock()bilan qayta yozing. assert_called_once_withga noto'g'ri matn bering va xatoni o'qing.create_autospecbilan mavjud bo'lmagan metodni chaqirib ko'ring.monkeypatchbilanvalyuta_kursini almashtiring.- Noto'g'ri joyda patch qilib, testni yiqiting.
- Uni to'g'ri joyda patch qilib tuzating.
XotiradagiOmborgaochirmetodini qo'shib, unga test yozing.- O'z loyihangizda beshta mock talab qiladigan test bormi - toping va sababini yozing.
Xulosa #
- Dublyor - tashqi bog'liqlikning test uchun qo'yilgan o'rinbosari.
- Kod bog'liqlikni o'zi yaratmasdan tashqaridan olsa, uni almashtirish oson bo'ladi.
- Beshta tur: dummy (joy to'ldiruvchi), stub (tayyor javob), spy (yozib boradi), mock (o'zi tekshiradi), fake (ishlaydigan soddalashtirilgan nusxa).
- Oddiy hollarda o'n qatorlik qo'lda yozilgan sinf yetarli - kutubxona shart emas.
unittest.mock.Mockhar qanday metodni qabul qiladi va chaqiruvlarni yozib boradi.Mock()nomidagi xatoni sezmaydi;create_autospecuni haqiqiy sinfga bog'laydi.monkeypatchmodul ichidagi nomni test davomida almashtiradi va o'zi qaytaradi.- Eng ko'p uchraydigan xato - patch ni noto'g'ri modulga qo'yish.
- Qoida: funksiya qaysi modulda chaqirilsa, o'sha moduldagi nomni almashtiring.
import moduluslubifrom modul import nomuslubiga qaraganda testlashga qulayroq.- Fake xatti-harakatni ifodalaydi: "saqlangandan keyin topish mumkin" - "saqla chaqirildi" dan kuchliroq dalil.
- Ko'p mock qo'yilgan test kodning ichki tuzilishiga bog'lanib qoladi va refaktoringda yiqiladi.
- Dublyor faqat tashqi chegarada kerak: tarmoq, to'lov, SMS, soat, tasodif.
- Bitta test uchun beshta mock kerak bo'lsa - muammo testda emas, kodning bog'lanishida.
Keyingi bo'limda eng qaysar bog'liqliklarga o'tamiz: joriy vaqt, tasodifiy sonlar va muhit o'zgaruvchilari.
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.