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.

🕑 14 daqiqa o‘qish 📄 968 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Beqarorlikning narxi
  2. Oltita sabab
  3. Tartibga bog'liqlikni ko'rsatish
  4. Tartibni ataylab aralashtirish
  5. Tuzatish
  6. Boshqa sabablarni topish
  7. Nima qilmaslik kerak
  8. Xulosa

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 #

Beqaror test qanday yo'ldan o'tadi Test yiqildi "g'alati..." Qayta ishga tushirildi endi o'tdi Odat shakllandi "yana qayta ishlat" Ishonch yo'qoldi testlar o'chiriladi Eng xavfli qadam - uchinchisi Shundan keyin haqiqiy xatoni ko'rsatgan test ham "yana o'sha beqaror test" deb o'tkazib yuboriladi Beqaror testni tuzatish yoki o'chirish kerak - uchinchi yo'l yo'q
Beqarorlik testlarga bo'lgan ishonchni yemiradi

Oltita sabab #

SababBelgisiYechim
Umumiy holatYolg'iz ishga tushirilganda boshqachaHar test o'z ma'lumotini yaratsin
Tartibga bog'liqlikTartib o'zgarsa yiqiladiBog'liqlikni uzish
VaqtYarim tunda yoki oy oxirida yiqiladiVaqtni argument qilish
TasodifHar o'ninchi marta yiqiladiUrug' belgilash
ParallellikFaqat -n auto bilan yiqiladiUmumiy resursni ajratish
Tashqi xizmatInternet sekin bo'lsa yiqiladiDublyor qo'yish

Birinchi ikkitasi eng ko'p uchraydi va ularni topish oson. Boshlaymiz.

Tartibga bog'liqlikni ko'rsatish #

Zararsiz ko'rinadigan kesh:

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

Terminal
pytest -q -p no:randomly test_tartib.py
Natija
.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:

Terminal
pytest -q -p no:randomly "test_tartib.py::test_kesh_bosh_boshlanadi" "test_tartib.py::test_birinchi_sorov_manbaga_boradi"
Natija
..                                                               [100%]
2 passed in 0.01s

Ikkalasi ham o'tdi. Bir xil kod, bir xil testlar, boshqa natija - faqat tartib o'zgardi.

Bu xato loyihada qanday paydo bo'ladi

Hech kim ataylab bunday yozmaydi. Odatda shunday bo'ladi:

  1. Birinchi test yoziladi - o'tadi.
  2. Ikkinchi test yoziladi - o'tadi, chunki u birinchisidan keyin ishlaydi.
  3. Oylar o'tadi. Kimdir uchinchi testni o'rtaga qo'shadi.
  4. Yoki CI parallel ishga tushirishni yoqadi.
  5. 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:

Terminal
pip install pytest-randomly
Natija
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:

Terminal
for urug in 1 2 3 4 5; do pytest -q -p randomly --randomly-seed=$urug test_tartib.py | tail -1; done
Natija
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.

Urug' qaytadan ishlatiladi

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:

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

Terminal
for urug in 1 2 3 4 5; do pytest -q -p randomly --randomly-seed=$urug test_tartib.py | tail -1; done
Natija
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 chora

Yuqoridagi fixture ishlaydi, lekin u alomatni yashiradi. Haqiqiy yechim - keshni modul darajasidan chiqarish:

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

ShubhaTekshirish usuli
Tartibpytest -p randomly bilan bir necha urug'
Umumiy holatTestni yolg'iz ishga tushiring
VaqtSoatni yarim tunga, oy oxiriga muzlating
Vaqt mintaqasiTZ=Pacific/Kiritimati pytest bilan ishga tushiring
TasodifUrug'ni o'zgartirib bir necha marta ishga tushiring
Parallellikpytest -n auto bilan ishga tushiring
Tashqi xizmatInternetni 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 #

Avtomatik qayta urinish - yechim emas

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:

  1. Takrorlang - urug' yoki tartibni belgilab, xatoni qayta chiqaring.
  2. Ajrating - qaysi test yoki qaysi resurs sabab ekanini toping.
  3. Tuzating - sababni yo'q qiling, alomatni emas.
  4. Agar darhol tuzatolmasangiz - karantinga oling: alohida belgi qo'yib, asosiy quvurdan chiqaring va muddat belgilang.

Karantin pytest.mark bilan qilinadi:

INI
[pytest]
markers =
    beqaror: hozircha beqaror, asosiy quvurda ishlamaydi
Python
import pytest


@pytest.mark.beqaror
def test_tashqi_xizmatga_boglangan():
    assert 1 + 1 == 2


def test_oddiy():
    assert 2 + 2 == 4
Terminal
pytest -q -p no:randomly -m "not beqaror" test_karantin.py
Natija
.                                                                [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.

Amaliy topshiriq
  1. kesh.py va ikkita testni yozib, ularni tartib bilan ishga tushiring.
  2. Tartibni teskari qilib, natija o'zgarganini ko'ring.
  3. pytest-randomly ni o'rnatib, beshta urug' bilan ishga tushiring.
  4. Yiqilgan urug'ni yozib olib, uni aynan takrorlang.
  5. autouse fixture bilan tuzating va beshta urug'ni qayta sinang.
  6. Keshni sinf ichiga ko'chirib, fixture'siz yechim yozing.
  7. TZ=Pacific/Kiritimati bilan o'z loyihangiz testlarini ishga tushiring.
  8. Internetni uzib, testlaringizni ishga tushiring - nechtasi yiqildi?
  9. beqaror belgisini e'lon qilib, bitta testni karantinga oling.
  10. 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-randomly har 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=True fixture 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.mark va -m "not beqaror" bilan qilinadi - test o'chirilmaydi, faqat quvurdan chiqariladi.

Keyingi bo'limda testi umuman yo'q kodga qanday yondashishni ko'ramiz.

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.