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.
Ushbu bo‘lim mundarijasi
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'liqlik | Nima uchun qiyin | Yechim |
|---|---|---|
| Joriy vaqt | Har sekundda boshqacha | Vaqtni argument qilib kiritish |
| Tasodifiy son | Har ishga tushirishda boshqacha | Urug' (seed) belgilash |
| Muhit o'zgaruvchisi | Har kompyuterda boshqacha | monkeypatch.setenv |
| Fayl tizimi | Qoldiqlar keyingi testga o'tadi | tmp_path |
Birinchisi eng ko'p uchraydi, shuning uchun undan boshlaymiz.
Vaqt: eng ko'p uchraydigan tuzoq #
Kutubxona kitobining kechikkan kunlarini hisoblaydigan kod:
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:
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:
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)
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
pytest -q test_vaqt.py
.... [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.
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:
pip install freezegun
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
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()}"
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"
pytest -q test_muzlatish.py
... [100%]
3 passed in 0.08s
freeze_time butun jarayon uchun date.today() va datetime.now()
javobini almashtiradi.
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.
Tasodif: urug' belgilash #
Tasodifiy sonlar bilan ishlaydigan kod:
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:
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)
pytest -q test_tasodif.py
..... [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.
Shunday yozish vasvasasi bo'ladi:
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:
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"
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"
pytest -q test_sozlama.py
.... [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:
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()
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"]
pytest -q test_jurnal.py
... [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.
Quyidagi qatorlar ertami-kechmi muammo keltiradi:
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.
date.today()ishlatadigan funksiyaga test yozing va u nima uchun ertaga yiqilishini tushuntiring.- Uni
bugun=Noneargumenti bilan qayta yozing. - Yil almashishi uchun test qo'shing (28-dekabrdan 3-yanvargacha).
- 29-fevral uchun ham test yozing - qaysi yillarda u bor?
freezegunni o'rnatib,@freeze_timebilan bitta test yozing.freeze_timeni blok ichida ishlatib ko'ring.- Tasodifiy tanlovga urug' berib, ikki marta bir xil natija olganingizni tekshiring.
- Aniq ro'yxatni tekshiradigan test yozing va u nimasi bilan xavfli ekanini yozing.
monkeypatch.setenvbilan sozlamani almashtiring vadelenvni ham sinang.tmp_pathbilan 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=Nonebilan 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.
freezegunsoatni 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.setenvvadelenvmuhit o'zgaruvchisini test davomida almashtiradi.delenv(..., raising=False)siz test sizda o'tib, CI da yiqilishi mumkin.tmp_pathhar 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.
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.