9-bo‘lim

Vaqt, tasodif va tashqi dunyo

Joriy vaqt, tasodifiy sonlar, muhit o'zgaruvchilari va fayl tizimi - testlashi eng qiyin to'rtta bog'liqlik va ularning yechimlari.

🕑 16 daqiqa o‘qish 📄 836 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. To'rtta qiyin bog'liqlik
  2. Vaqt: eng ko'p uchraydigan tuzoq
  3. Yechim 1: vaqtni kiritish
  4. Yechim 2: soatni to'xtatish
  5. Tasodif: urug' belgilash
  6. Muhit o'zgaruvchisi
  7. Fayl tizimi
  8. Xulosa

Ba'zi testlar bugun o'tadi, ertaga yiqiladi. Ba'zilari sizda ishlaydi, hamkasbingizda yo'q. Ba'zilari o'n martadan to'qqiztasida o'tadi.

Deyarli har doim sabab bitta: kod tashqi dunyoga murojaat qilyapti.

To'rtta qiyin bog'liqlik #

Bog'liqlikNima uchun qiyinYechim
Joriy vaqtHar sekundda boshqachaVaqtni argument qilib kiritish
Tasodifiy sonHar ishga tushirishda boshqachaUrug' (seed) belgilash
Muhit o'zgaruvchisiHar kompyuterda boshqachamonkeypatch.setenv
Fayl tizimiQoldiqlar keyingi testga o'taditmp_path

Birinchisi eng ko'p uchraydi, shuning uchun undan boshlaymiz.

Vaqt: eng ko'p uchraydigan tuzoq #

Kutubxona kitobining kechikkan kunlarini hisoblaydigan kod:

Python
from datetime import date


def kechikkan_kunlar(qaytarish_sanasi):
    farq = (date.today() - qaytarish_sanasi).days
    return max(0, farq)

Unga test yozish uchun... bugungi sanani bilish kerak. Test shunday yoziladi:

Python
from datetime import date

from jarima import kechikkan_kunlar


def test_uch_kun_kechikkan():
    assert kechikkan_kunlar(date(2026, 9, 20)) == 3

Bu test faqat 2026-yil 23-sentabrda o'tadi. Ertasiga u yiqiladi va hech kim nima bo'lganini tushunmaydi.

Yechim 1: vaqtni kiritish #

Eng sodda va eng kuchli yechim - joriy vaqtni argument qilish:

Python
from datetime import date


def kechikkan_kunlar(qaytarish_sanasi, bugun=None):
    if bugun is None:
        bugun = date.today()
    return max(0, (bugun - qaytarish_sanasi).days)
Python
from datetime import date

from jarima import kechikkan_kunlar


def test_uch_kun_kechikkan():
    assert kechikkan_kunlar(date(2026, 9, 20), bugun=date(2026, 9, 23)) == 3


def test_ayni_kunda_kechikish_yoq():
    assert kechikkan_kunlar(date(2026, 9, 23), bugun=date(2026, 9, 23)) == 0


def test_muddatdan_oldin_qaytarilsa_nol():
    assert kechikkan_kunlar(date(2026, 9, 30), bugun=date(2026, 9, 23)) == 0


def test_yil_almashishi():
    assert kechikkan_kunlar(date(2025, 12, 28), bugun=date(2026, 1, 3)) == 6
Terminal
pytest -q test_vaqt.py
Natija
....                                                             [100%]
4 passed in 0.01s

To'rtta test, ularning hammasi har doim bir xil natija beradi. Va diqqat qiling: oxirgi testni yozish endi oson bo'ldi - yil almashishi kabi chegarani sinash uchun yanvargacha kutish shart emas.

Bu yechim kodni ham yaxshilaydi

bugun=None qo'shilgani - test uchun qilingan "chetlab o'tish" emas. U kodni haqiqatda foydaliroq qiladi:

  • o'tmishdagi hisobotni qayta hisoblash mumkin bo'ladi;
  • boshqa vaqt mintaqasi uchun hisoblash mumkin bo'ladi;
  • funksiya endi toza - bir xil kirishga har doim bir xil javob.

Bu umumiy qoidaning misoli: *testlash qiyin kod - odatda dizayni yomon kod*. Testni osonlashtirish uchun qilingan o'zgarish deyarli har doim kodni ham yaxshilaydi.

Yechim 2: soatni to'xtatish #

Eski kodda vaqtni argument qilish har doim ham mumkin emas. Shunda soatning o'zini "to'xtatish" mumkin. Buning uchun freezegun kutubxonasi ishlatiladi:

Terminal
pip install freezegun
Natija
Collecting freezegun
  Downloading freezegun-1.5.5-py3-none-any.whl.metadata (13 kB)
Collecting python-dateutil>=2.7 (from freezegun)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting six>=1.5 (from python-dateutil>=2.7->freezegun)
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading freezegun-1.5.5-py3-none-any.whl (19 kB)
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Installing collected packages: six, python-dateutil, freezegun

Successfully installed freezegun-1.5.5 python-dateutil-2.9.0.post0 six-1.17.0
Python
from datetime import date


def kechikkan_kunlar(qaytarish_sanasi, bugun=None):
    if bugun is None:
        bugun = date.today()
    return max(0, (bugun - qaytarish_sanasi).days)


def bugungi_hisobot():
    return f"Hisobot sanasi: {date.today().isoformat()}"
Python
from datetime import date

from freezegun import freeze_time

from jarima import bugungi_hisobot, kechikkan_kunlar


@freeze_time("2026-09-23")
def test_hisobot_sanasi():
    assert bugungi_hisobot() == "Hisobot sanasi: 2026-09-23"


@freeze_time("2026-09-23")
def test_argumentsiz_chaqiruv():
    assert kechikkan_kunlar(date(2026, 9, 20)) == 3


def test_blok_ichida_muzlatish():
    with freeze_time("2027-01-01"):
        assert bugungi_hisobot() == "Hisobot sanasi: 2027-01-01"
Terminal
pytest -q test_muzlatish.py
Natija
...                                                              [100%]
3 passed in 0.08s

freeze_time butun jarayon uchun date.today() va datetime.now() javobini almashtiradi.

Muzlatish - ikkinchi tanlov

freezegun kuchli vosita, lekin u alomatni davolaydi, sababni emas. U kerak bo'lgan joylar:

  • o'zgartirib bo'lmaydigan eski kod;
  • uchinchi tomon kutubxonasi ichidagi vaqt;
  • juda ko'p joyda now() chaqirilgan holat.

Yangi kod yozayotgan bo'lsangiz, vaqtni argument qiling. Bu tezroq ham: muzlatish har testga qo'shimcha yuk qo'shadi.

Toza yadro, iflos qobiq Toza yadro hisob-kitob Iflos qobiq soat tasodif fayl tarmoq baza Yadro: ko'p modul testi tez, ishonchli, dublyorsiz Qobiq: kam integratsiya testi sekin, lekin ular oz Tashqi dunyo chekkada qolsa, kodning katta qismi oson testlanadi
Vaqt, tasodif va tarmoqni markazdan chekkaga suring

Tasodif: urug' belgilash #

Tasodifiy sonlar bilan ishlaydigan kod:

Python
import random


def savol_tanla(savollar, soni, tasodif=None):
    """Imtihon uchun tasodifiy savollar tanlaydi."""
    if tasodif is None:
        tasodif = random
    if soni > len(savollar):
        raise ValueError("savollar yetarli emas")
    return tasodif.sample(savollar, soni)

Bu yerda ham bir xil naqsh: tasodif manbai tashqaridan berilishi mumkin. Testda biz unga belgilangan urug'li generator beramiz:

Python
import random

import pytest

from tanlov import savol_tanla

SAVOLLAR = ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8"]


def test_belgilangan_urug_bir_xil_natija_beradi():
    birinchi = savol_tanla(SAVOLLAR, 3, tasodif=random.Random(42))
    ikkinchi = savol_tanla(SAVOLLAR, 3, tasodif=random.Random(42))

    assert birinchi == ikkinchi


def test_kerakli_miqdorda_savol_qaytadi():
    natija = savol_tanla(SAVOLLAR, 3, tasodif=random.Random(7))

    assert len(natija) == 3


def test_savollar_takrorlanmaydi():
    natija = savol_tanla(SAVOLLAR, 5, tasodif=random.Random(7))

    assert len(set(natija)) == 5


def test_hammasi_royxatdan_olinadi():
    natija = savol_tanla(SAVOLLAR, 4, tasodif=random.Random(7))

    assert set(natija) <= set(SAVOLLAR)


def test_yetarli_savol_yoq():
    with pytest.raises(ValueError, match="yetarli emas"):
        savol_tanla(SAVOLLAR, 20)
Terminal
pytest -q test_tasodif.py
Natija
.....                                                            [100%]
5 passed in 0.01s

Bu yerda muhim fikr bor. Birinchi test urug'ni belgilab, aniq takrorlanishni tekshiradi. Qolganlari esa aniq natijani emas, xossalarni tekshiradi: uzunligi, takrorlanmasligi, manbadan olinganligi.

Tasodifiy chiqishda aniq qiymatni yozib qo'ymang

Shunday yozish vasvasasi bo'ladi:

Python
assert savol_tanla(SAVOLLAR, 3, tasodif=random.Random(42)) == ["s2", "s7", "s1"]

Test o'tadi - bugun. Lekin Python ning random moduli ichki algoritmini o'zgartirsa yoki siz sample o'rniga choices ga o'tsangiz, test yiqiladi. Holbuki kod to'g'ri ishlashda davom etadi.

Xossalarni tekshiring: nechta, takrorlanmaydimi, ro'yxatdan olinganmi. Bu yondashuvning to'liq shakli - 17-bo'limdagi xossaviy testlash.

Muhit o'zgaruvchisi #

Sozlamalar odatda muhitdan o'qiladi - va har kompyuterda boshqacha bo'ladi:

Python
import os


def baza_manzili():
    host = os.environ.get("BAZA_HOST", "localhost")
    port = os.environ.get("BAZA_PORT", "5432")
    return f"{host}:{port}"


def rejim():
    if os.environ.get("ISHLAB_CHIQARISH") == "1":
        return "ishlab chiqarish"
    return "sinov"
Python
from sozlama import baza_manzili, rejim


def test_sukut_qiymatlar(monkeypatch):
    monkeypatch.delenv("BAZA_HOST", raising=False)
    monkeypatch.delenv("BAZA_PORT", raising=False)

    assert baza_manzili() == "localhost:5432"


def test_muhitdan_oqiladi(monkeypatch):
    monkeypatch.setenv("BAZA_HOST", "10.0.0.5")
    monkeypatch.setenv("BAZA_PORT", "6543")

    assert baza_manzili() == "10.0.0.5:6543"


def test_ishlab_chiqarish_rejimi(monkeypatch):
    monkeypatch.setenv("ISHLAB_CHIQARISH", "1")

    assert rejim() == "ishlab chiqarish"


def test_sinov_rejimi(monkeypatch):
    monkeypatch.delenv("ISHLAB_CHIQARISH", raising=False)

    assert rejim() == "sinov"
Terminal
pytest -q test_sozlama.py
Natija
....                                                             [100%]
4 passed in 0.01s

raising=False - o'zgaruvchi umuman yo'q bo'lsa ham xato bermasin degani. Usiz test sizning kompyuteringizda o'tib, CI da yiqilishi mumkin.

Fayl tizimi #

Fayl bilan ishlaydigan testlarda ikkita xavf bor: qoldiqlar va kompyuterlar orasidagi farq. tmp_path ikkalasini ham hal qiladi:

Python
from pathlib import Path


def yoz(papka, satr):
    fayl = Path(papka) / "jurnal.txt"
    with fayl.open("a", encoding="utf-8") as f:
        f.write(satr + "\n")
    return fayl


def oqi(papka):
    fayl = Path(papka) / "jurnal.txt"
    if not fayl.exists():
        return []
    return fayl.read_text(encoding="utf-8").splitlines()
Python
from jurnal import oqi, yoz


def test_bosh_papkada_jurnal_yoq(tmp_path):
    assert oqi(tmp_path) == []


def test_yozilgan_satr_oqiladi(tmp_path):
    yoz(tmp_path, "birinchi")

    assert oqi(tmp_path) == ["birinchi"]


def test_satrlar_ketma_ket_qoshiladi(tmp_path):
    yoz(tmp_path, "birinchi")
    yoz(tmp_path, "ikkinchi")

    assert oqi(tmp_path) == ["birinchi", "ikkinchi"]
Terminal
pytest -q test_jurnal.py
Natija
...                                                              [100%]
3 passed in 0.01s

Uchta test, uchtasi ham "jurnal.txt" bilan ishlaydi - lekin ular bir-birining faylini ko'rmaydi. tmp_path har test uchun yangi papka beradi. Birinchi test bo'sh papkani ko'rgani ham shuning dalili.

Testda haqiqiy yo'llarni yozmang

Quyidagi qatorlar ertami-kechmi muammo keltiradi:

Python
yoz("/tmp/jurnal", "satr")                  # Windows da ishlamaydi
yoz("C:/Users/aziza/hujjat", "satr")        # faqat bitta odamda ishlaydi
yoz("./test_fayllar", "satr")               # qoldiq qoladi, keyingi test yiqiladi

tmp_path uchala muammoni ham hal qiladi: u har tizimda to'g'ri ishlaydi, har test uchun yangi va keyin avtomatik tozalanadi.

Amaliy topshiriq
  1. date.today() ishlatadigan funksiyaga test yozing va u nima uchun ertaga yiqilishini tushuntiring.
  2. Uni bugun=None argumenti bilan qayta yozing.
  3. Yil almashishi uchun test qo'shing (28-dekabrdan 3-yanvargacha).
  4. 29-fevral uchun ham test yozing - qaysi yillarda u bor?
  5. freezegun ni o'rnatib, @freeze_time bilan bitta test yozing.
  6. freeze_time ni blok ichida ishlatib ko'ring.
  7. Tasodifiy tanlovga urug' berib, ikki marta bir xil natija olganingizni tekshiring.
  8. Aniq ro'yxatni tekshiradigan test yozing va u nimasi bilan xavfli ekanini yozing.
  9. monkeypatch.setenv bilan sozlamani almashtiring va delenv ni ham sinang.
  10. tmp_path bilan uchta test yozib, ularning bir-birini ko'rmasligiga ishonch hosil qiling.

Xulosa #

  • Beqaror testlarning asosiy sababi - kodning tashqi dunyoga murojaat qilishi.
  • To'rtta eng qiyin bog'liqlik: joriy vaqt, tasodifiy son, muhit o'zgaruvchisi va fayl tizimi.
  • date.today() ga tayangan test faqat bitta kunda o'tadi va ertasiga yiqiladi.
  • Eng yaxshi yechim - vaqtni argument qilish: bugun=None bilan sukut qiymat qoldiring.
  • Bu o'zgarish kodni ham yaxshilaydi: o'tmishdagi hisobotni qayta hisoblash imkoni paydo bo'ladi.
  • Umumiy qoida: testlash qiyin kod - odatda dizayni yomon kod.
  • freezegun soatni to'xtatadi; u eski kod uchun ikkinchi tanlov, yangi kod uchun emas.
  • Tasodif manbaini ham tashqaridan bering: random.Random(42) takrorlanadigan natija beradi.
  • Tasodifiy chiqishda aniq qiymatni emas, xossalarni tekshiring: nechta, takrorlanadimi, manbadanmi.
  • monkeypatch.setenv va delenv muhit o'zgaruvchisini test davomida almashtiradi.
  • delenv(..., raising=False) siz test sizda o'tib, CI da yiqilishi mumkin.
  • tmp_path har test uchun yangi papka beradi va uni o'zi tozalaydi.
  • Testda haqiqiy yo'llarni (/tmp/..., C:/Users/..., ./papka) yozmang.
  • Umumiy tamoyil: tashqi dunyoni chekkaga suring, yadroda faqat toza hisob-kitob qolsin.

Keyingi bo'limda tartibni teskari qilamiz: avval test, keyin kod.

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.