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.

🕑 17 daqiqa o‘qish 📄 1 045 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo
  2. Beshta dublyor turi
  3. Stub va spy - qo'lda yozilgani
  4. unittest.mock - tayyor dublyor
  5. monkeypatch - modul ichidagi narsani almashtirish
  6. Eng ko'p uchraydigan xato: patch qayerga qo'yiladi
  7. Fake - ishlaydigan soddalashtirilgan nusxa
  8. Dublyor qachon zarar keltiradi
  9. Xulosa

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:

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

TurNima qiladiQachon kerak
DummyHech narsa qilmaydi, faqat joyni to'ldiradiArgument talab qilinadi, lekin ishlatilmaydi
StubOldindan belgilangan javob qaytaradiKodga ma'lum bir javob kerak
SpyJavob beradi va chaqiruvlarni yozib boradiChaqirilganini tekshirish kerak
MockChaqiruvlarni oldindan kutadi va tekshiradiMuloqotning o'zi muhim
FakeSoddalashtirilgan, lekin ishlaydigan nusxaHaqiqiy narsa juda sekin
Dublyorlar: soddadan murakkabga Dummy Bo'sh qobiq Hech qachon chaqirilmaydi Stub Tayyor javob qaytaradi return 42 Spy Javob beradi va chaqiruvlarni yozib boradi Mock Kutilgan chaqiruvlarni o'zi tekshiradi Fake Haqiqatda ishlaydi, lekin soddalashtirilgan Kundalik gapda hammasi "mock" deb ataladi - lekin farqni bilish testni to'g'ri yozishga yordam beradi
Chapdan o'ngga: dublyor qanchalik ko'p narsa biladi

Stub va spy - qo'lda yozilgani #

Eng oddiy dublyorlar uchun hech qanday kutubxona kerak emas:

Python
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 == []
Terminal
pytest -q test_qolda.py
Natija
..                                                               [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:

Python
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
Terminal
pytest -q test_mock.py
Natija
...                                                              [100%]
3 passed in 0.04s

Eng kerakli metodlar:

MetodNimani 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_countNecha marta chaqirilganini
call_args_listBarcha chaqiruvlar ro'yxatini
return_valueDublyor nima qaytarishini belgilaydi
side_effectChaqiruvda istisno tashlashni yoki har safar boshqa javobni
Mock hamma narsaga "ha" deydi

Mock() obyektida istalgan metodni chaqirsangiz - u ishlaydi. Nomida xato bo'lsa ham:

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

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

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

Python
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
Terminal
pytest -q test_monkeypatch.py
Natija
..                                                               [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.

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

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

Python
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"
Terminal
pytest -q --tb=short test_notogri_patch.py
Natija
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:

Python
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"
Terminal
pytest -q test_togri_patch.py
Natija
.                                                                [100%]
1 passed in 0.02s
Qoida: e'lon qilingan joyda emas, ishlatilgan joyda patch qiling

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 uslubiPatch qayerga
from kurs import valyuta_kursihisobot.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:

Python
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"]
Python
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"})
Terminal
pytest -q test_fake.py
Natija
..                                                               [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 #

Ko'p mock qo'yilgan test nimani tekshiradi? Ko'p mock bilan • Test kodning ichki tuzilishini biladi • Refaktoring qilsangiz - test yiqiladi • Kod buzilsa ham test o'tishi mumkin • Test kodni emas, o'zini tekshiradi Kam mock bilan • Test faqat natijani biladi • Ichki tuzilish erkin o'zgaradi • Kod buzilsa - test darhol aytadi • Test xatti-harakatni tekshiradi Dublyor faqat tashqi chegarada kerak: tarmoq, to'lov, SMS, soat, tasodif
Mock - vosita, maqsad emas
Dublyor qo'yingDublyor qo'ymang
Tarmoq so'rovlarigaO'z kodingizning ichki funksiyalariga
To'lov va SMS xizmatlarigaOddiy hisob-kitoblarga
Joriy vaqt va tasodifgaMa'lumotlar tuzilmalariga
Juda sekin amallargaTez ishlaydigan narsalarga
Chaqirib bo'lmaydigan xatolargaFaqat "toza ajratish" uchun
Mock soni - dizayn haqidagi signal

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.

Amaliy topshiriq
  1. xabar.py ni yozib, qo'lda spy sinfi bilan ikkita testni ishga tushiring.
  2. Spy ga uchinchi test qo'shing: ikki mijozga xabar ketishini tekshiring.
  3. Xuddi shu testni Mock() bilan qayta yozing.
  4. assert_called_once_with ga noto'g'ri matn bering va xatoni o'qing.
  5. create_autospec bilan mavjud bo'lmagan metodni chaqirib ko'ring.
  6. monkeypatch bilan valyuta_kursi ni almashtiring.
  7. Noto'g'ri joyda patch qilib, testni yiqiting.
  8. Uni to'g'ri joyda patch qilib tuzating.
  9. XotiradagiOmbor ga ochir metodini qo'shib, unga test yozing.
  10. 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.Mock har qanday metodni qabul qiladi va chaqiruvlarni yozib boradi.
  • Mock() nomidagi xatoni sezmaydi; create_autospec uni haqiqiy sinfga bog'laydi.
  • monkeypatch modul 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 modul uslubi from modul import nom uslubiga 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.

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.