15-bo‘lim
Beqaror (flaky) testlar
Goh o'tib, goh yiqiladigan testlar, ularning oltita sababi, tartibga bog'liqlikni pytest-randomly bilan topish va beqarorlikni tuzatish tartibi.
Ushbu bo‘lim mundarijasi
Beqaror test - kod o'zgarmagan holda goh o'tadigan, goh yiqiladigan test.
U oddiy xatodan ko'ra xavfliroq. Chunki yiqilgan test tuzatiladi, beqaror test esa e'tiborsiz qoldiriladi - va bir kuni u haqiqiy xatoni ko'rsatganda ham hech kim ishonmaydi.
Beqarorlikning narxi #
Oltita sabab #
| Sabab | Belgisi | Yechim |
|---|---|---|
| Umumiy holat | Yolg'iz ishga tushirilganda boshqacha | Har test o'z ma'lumotini yaratsin |
| Tartibga bog'liqlik | Tartib o'zgarsa yiqiladi | Bog'liqlikni uzish |
| Vaqt | Yarim tunda yoki oy oxirida yiqiladi | Vaqtni argument qilish |
| Tasodif | Har o'ninchi marta yiqiladi | Urug' belgilash |
| Parallellik | Faqat -n auto bilan yiqiladi | Umumiy resursni ajratish |
| Tashqi xizmat | Internet sekin bo'lsa yiqiladi | Dublyor qo'yish |
Birinchi ikkitasi eng ko'p uchraydi va ularni topish oson. Boshlaymiz.
Tartibga bog'liqlikni ko'rsatish #
Zararsiz ko'rinadigan kesh:
_KESH = {}
def narx_ol(mahsulot, manba):
"""Narxni manbadan oladi va keshlab qo'yadi."""
if mahsulot not in _KESH:
_KESH[mahsulot] = manba(mahsulot)
return _KESH[mahsulot]
def kesh_hajmi():
return len(_KESH)
from kesh import kesh_hajmi, narx_ol
def test_birinchi_sorov_manbaga_boradi():
chaqiruvlar = []
def manba(nom):
chaqiruvlar.append(nom)
return 5000
narx_ol("non", manba)
assert chaqiruvlar == ["non"]
def test_kesh_bosh_boshlanadi():
assert kesh_hajmi() == 0
Tartib bilan ishga tushiramiz:
pytest -q -p no:randomly test_tartib.py
.F [100%]
=============================== FAILURES ===============================
______________________ test_kesh_bosh_boshlanadi _______________________
def test_kesh_bosh_boshlanadi():
> assert kesh_hajmi() == 0
E assert 1 == 0
E + where 1 = kesh_hajmi()
test_tartib.py:17: AssertionError
======================= short test summary info ========================
FAILED test_tartib.py::test_kesh_bosh_boshlanadi - assert 1 == 0
1 failed, 1 passed in 0.01s
Ikkinchi test yiqildi: birinchisi keshga yozib qo'ygan va kesh modul darajasida - ya'ni testlar orasida yashaydi.
Endi xuddi shu ikkita testni teskari tartibda ishga tushiramiz:
pytest -q -p no:randomly "test_tartib.py::test_kesh_bosh_boshlanadi" "test_tartib.py::test_birinchi_sorov_manbaga_boradi"
.. [100%]
2 passed in 0.01s
Ikkalasi ham o'tdi. Bir xil kod, bir xil testlar, boshqa natija - faqat tartib o'zgardi.
Hech kim ataylab bunday yozmaydi. Odatda shunday bo'ladi:
- Birinchi test yoziladi - o'tadi.
- Ikkinchi test yoziladi - o'tadi, chunki u birinchisidan keyin ishlaydi.
- Oylar o'tadi. Kimdir uchinchi testni o'rtaga qo'shadi.
- Yoki CI parallel ishga tushirishni yoqadi.
- Testlar sababsiz yiqila boshlaydi.
O'sha paytda sababni topish juda qiyin bo'ladi - chunki o'zgargan narsa test ham, kod ham emas, tartib.
Tartibni ataylab aralashtirish #
Bunday xatolarni topishning eng oson yo'li - testlarni har safar boshqa tartibda ishga tushirish:
pip install pytest-randomly
Collecting pytest-randomly
Downloading pytest_randomly-5.0.0-py3-none-any.whl.metadata (8.9 kB)
Requirement already satisfied: pytest>=8 in ./.venv/lib/python3.13/site-packages (from pytest-randomly) (9.1.1)
Requirement already satisfied: iniconfig>=1.0.1 in ./.venv/lib/python3.13/site-packages (from pytest>=8->pytest-randomly) (2.3.0)
Requirement already satisfied: packaging>=22 in ./.venv/lib/python3.13/site-packages (from pytest>=8->pytest-randomly) (26.3)
Requirement already satisfied: pluggy<2,>=1.5 in ./.venv/lib/python3.13/site-packages (from pytest>=8->pytest-randomly) (1.6.0)
Requirement already satisfied: pygments>=2.7.2 in ./.venv/lib/python3.13/site-packages (from pytest>=8->pytest-randomly) (2.21.0)
Downloading pytest_randomly-5.0.0-py3-none-any.whl (8.9 kB)
Installing collected packages: pytest-randomly
Successfully installed pytest-randomly-5.0.0
O'rnatilgan zahoti u ishlay boshlaydi: har ishga tushirishda tartib aralashtiriladi va ishlatilgan urug' chiqishda ko'rsatiladi. Bir necha urug'ni sinab ko'ramiz:
for urug in 1 2 3 4 5; do pytest -q -p randomly --randomly-seed=$urug test_tartib.py | tail -1; done
1 failed, 1 passed in 0.01s
1 failed, 1 passed in 0.01s
2 passed in 0.01s
2 passed in 0.01s
1 failed, 1 passed in 0.01s
Ba'zi urug'larda ikkala test o'tdi, ba'zilarida bittasi yiqildi. Aynan shu - beqaror testning ta'rifi.
Chiqishda Using --randomly-seed=1234 degan qator bo'ladi. Yiqilgan
ishga tushirishni aynan takrorlash uchun shu urug'ni bering:
pytest --randomly-seed=1234
Bu beqaror testni tuzatishdagi eng muhim vosita: siz xatoni takrorlay olmasangiz, uni tuzatganingizni ham bilolmaysiz.
Tuzatish #
Sabab - kesh modul darajasida yashayotgani. Uni har test uchun tozalaymiz:
import pytest
import kesh
@pytest.fixture(autouse=True)
def toza_kesh():
kesh._KESH.clear()
yield
kesh._KESH.clear()
autouse=True - bu fixture ni har test so'ramasdan oladi.
for urug in 1 2 3 4 5; do pytest -q -p randomly --randomly-seed=$urug test_tartib.py | tail -1; done
2 passed in 0.01s
2 passed in 0.01s
2 passed in 0.01s
2 passed in 0.01s
2 passed in 0.01s
Beshta urug'da ham bir xil natija. Test endi barqaror.
autouse - vaqtinchalik choraYuqoridagi fixture ishlaydi, lekin u alomatni yashiradi. Haqiqiy yechim - keshni modul darajasidan chiqarish:
class NarxXizmati:
def __init__(self, manba):
self._kesh = {}
self._manba = manba
def narx_ol(self, mahsulot):
if mahsulot not in self._kesh:
self._kesh[mahsulot] = self._manba(mahsulot)
return self._kesh[mahsulot]
Endi har test o'z NarxXizmati sini yaratadi va hech qanday fixture
kerak emas. Bu 9-bo'limdagi qoidaning takrori: testlash qiyin kod -
dizayni yomon kod.
autouse esa o'zgartirib bo'lmaydigan eski kod uchun qoladi.
Boshqa sabablarni topish #
| Shubha | Tekshirish usuli |
|---|---|
| Tartib | pytest -p randomly bilan bir necha urug' |
| Umumiy holat | Testni yolg'iz ishga tushiring |
| Vaqt | Soatni yarim tunga, oy oxiriga muzlating |
| Vaqt mintaqasi | TZ=Pacific/Kiritimati pytest bilan ishga tushiring |
| Tasodif | Urug'ni o'zgartirib bir necha marta ishga tushiring |
| Parallellik | pytest -n auto bilan ishga tushiring |
| Tashqi xizmat | Internetni uzib ishga tushiring |
Oxirgi ikkitasi ayniqsa foydali: agar testlar internetsiz yiqilsa - demak ular tashqi xizmatga bog'langan va bu har doim beqarorlik manbai.
Nima qilmaslik kerak #
Ba'zi vositalar testni yiqilganda avtomatik qayta ishga tushiradi
(pytest-rerunfailures kabi). Bu vasvasali, lekin u muammoni
yashiradi:
- beqaror test yashil ko'rinadi va hech kim uni tuzatmaydi;
- testlar sekinlashadi (yiqilgani uch marta ishlaydi);
- eng yomoni - haqiqiy, lekin kamdan-kam uchraydigan xato ham shu yo'l bilan yashiriladi.
Agar test o'n martadan to'qqiztasida o'tsa, demak kodingiz ham o'n martadan to'qqiztasida ishlaydi. Bu test muammosi emas, kod muammosi bo'lishi mumkin.
To'g'ri tartib quyidagicha:
- Takrorlang - urug' yoki tartibni belgilab, xatoni qayta chiqaring.
- Ajrating - qaysi test yoki qaysi resurs sabab ekanini toping.
- Tuzating - sababni yo'q qiling, alomatni emas.
- Agar darhol tuzatolmasangiz - karantinga oling: alohida belgi qo'yib, asosiy quvurdan chiqaring va muddat belgilang.
Karantin pytest.mark bilan qilinadi:
[pytest]
markers =
beqaror: hozircha beqaror, asosiy quvurda ishlamaydi
import pytest
@pytest.mark.beqaror
def test_tashqi_xizmatga_boglangan():
assert 1 + 1 == 2
def test_oddiy():
assert 2 + 2 == 4
pytest -q -p no:randomly -m "not beqaror" test_karantin.py
. [100%]
1 passed, 1 deselected in 0.00s
Bitta test tanlanmadi - u karantinda. Asosiy quvur endi toza ishlaydi, lekin test o'chirilmagan va uni tuzatish vazifasi yo'qolmaydi.
kesh.pyva ikkita testni yozib, ularni tartib bilan ishga tushiring.- Tartibni teskari qilib, natija o'zgarganini ko'ring.
pytest-randomlyni o'rnatib, beshta urug' bilan ishga tushiring.- Yiqilgan urug'ni yozib olib, uni aynan takrorlang.
autousefixture bilan tuzating va beshta urug'ni qayta sinang.- Keshni sinf ichiga ko'chirib, fixture'siz yechim yozing.
TZ=Pacific/Kiritimatibilan o'z loyihangiz testlarini ishga tushiring.- Internetni uzib, testlaringizni ishga tushiring - nechtasi yiqildi?
beqarorbelgisini e'lon qilib, bitta testni karantinga oling.- Loyihangizda oxirgi oyda "qayta ishga tushirilgan" test bormi - eslang.
Xulosa #
- Beqaror test - kod o'zgarmagan holda goh o'tadigan, goh yiqiladigan test.
- U oddiy xatodan xavfliroq: yiqilgan test tuzatiladi, beqaror test e'tiborsiz qoldiriladi.
- Eng xavfli qadam - "yana qayta ishga tushiraman" odatining shakllanishi.
- Oltita asosiy sabab: umumiy holat, tartibga bog'liqlik, vaqt, tasodif, parallellik, tashqi xizmat.
- Tartibga bog'liqlik odatda ataylab emas, vaqt o'tishi bilan paydo bo'ladi.
pytest-randomlyhar ishga tushirishda tartibni aralashtiradi va ishlatilgan urug'ni ko'rsatadi.- Urug'ni qaytadan berib (
--randomly-seed=1234), yiqilgan ishga tushirishni aynan takrorlash mumkin. - Xatoni takrorlay olmasangiz, uni tuzatganingizni ham bilolmaysiz.
autouse=Truefixture har test uchun holatni tozalaydi, lekin bu vaqtinchalik chora.- Haqiqiy yechim - umumiy holatni modul darajasidan chiqarib, obyekt ichiga solish.
- Boshqa sabablarni topish uchun: vaqt mintaqasini almashtiring, parallel ishlating, internetni uzing.
- Testlar internetsiz yiqilsa - demak ular tashqi xizmatga bog'langan.
- Avtomatik qayta urinish yechim emas: u beqarorlikni ham, haqiqiy kamdan-kam xatoni ham yashiradi.
- To'g'ri tartib: takrorlang, ajrating, sababni tuzating; darhol bo'lmasa - karantinga oling.
- Karantin
pytest.markva-m "not beqaror"bilan qilinadi - test o'chirilmaydi, faqat quvurdan chiqariladi.
Keyingi bo'limda testi umuman yo'q kodga qanday yondashishni ko'ramiz.
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.