20-bo‘lim

Yakuniy loyiha - testi yo'q loyihaga to'liq to'plam

Bitta testi yo'q xarajat hisobchisini olib, unga piramida bo'yicha modul, integratsiya, API va dublyor testlarini qurish, qoplamani yopish va CI ga ulash.

🕑 22 daqiqa o‘qish 📄 812 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Loyiha bilan tanishish
  2. Reja
  3. 1-qadam: modul testlari
  4. 2-qadam: dublyor bilan xabarnoma
  5. 3-qadam: integratsiya testlari
  6. 4-qadam: API testlari
  7. 5-qadam: qoplamani tekshirish
  8. 6-qadam: belgilar va CI
  9. Nimaga erishdik
  10. Xulosa

Endi hamma narsani birlashtiramiz. Bizda kichik loyiha bor - xarajat hisobchisi - va unda bitta ham test yo'q.

Vazifa: unga ishonchli test to'plami qurish. Har qadamda oldingi bo'limlardagi qoidalarni qo'llaymiz.

Loyiha bilan tanishish #

Uchta modul: qoidalar, ombor va veb interfeys.

Python
from datetime import date

TURKUMLAR = ("oziq-ovqat", "transport", "uy", "salomatlik", "boshqa")
OYLIK_CHEGARA = 3_000_000


def xarajat_yarat(nom, summa, turkum, sana):
    """Bitta xarajat yozuvini tekshirib yaratadi."""
    if not nom or not nom.strip():
        raise ValueError("nom bo'sh bo'lishi mumkin emas")
    if summa <= 0:
        raise ValueError("summa musbat bo'lishi kerak")
    if turkum not in TURKUMLAR:
        raise ValueError(f"noma'lum turkum: {turkum}")
    if not isinstance(sana, date):
        raise TypeError("sana date turida bo'lishi kerak")
    return {"nom": nom.strip(), "summa": summa, "turkum": turkum, "sana": sana}


def oylik_jami(xarajatlar, yil, oy):
    """Berilgan oydagi xarajatlar yig'indisi."""
    return sum(
        x["summa"] for x in xarajatlar
        if x["sana"].year == yil and x["sana"].month == oy
    )


def turkum_boyicha(xarajatlar):
    """Turkumlar kesimida yig'indi; bo'sh turkumlar qaytarilmaydi."""
    natija = {}
    for xarajat in xarajatlar:
        natija[xarajat["turkum"]] = natija.get(xarajat["turkum"], 0) + xarajat["summa"]
    return natija


def ogohlantirish_darajasi(jami, chegara=OYLIK_CHEGARA):
    """Chegaraga nisbatan holat: yashil, sariq yoki qizil."""
    if chegara <= 0:
        raise ValueError("chegara musbat bo'lishi kerak")
    foiz = jami / chegara * 100
    if foiz >= 100:
        return "qizil"
    if foiz >= 70:
        return "sariq"
    return "yashil"
Python
import sqlite3
from datetime import date

SXEMA = """
CREATE TABLE xarajat (
    id     INTEGER PRIMARY KEY,
    nom    TEXT    NOT NULL,
    summa  INTEGER NOT NULL,
    turkum TEXT    NOT NULL,
    sana   TEXT    NOT NULL
);
"""


def ulanish_och(yol):
    ulanish = sqlite3.connect(yol)
    ulanish.row_factory = sqlite3.Row
    return ulanish


def sxemani_yarat(ulanish):
    ulanish.executescript(SXEMA)


def saqla(ulanish, xarajat):
    kursor = ulanish.execute(
        "INSERT INTO xarajat (nom, summa, turkum, sana) VALUES (?, ?, ?, ?)",
        (xarajat["nom"], xarajat["summa"], xarajat["turkum"], xarajat["sana"].isoformat()),
    )
    ulanish.commit()
    return kursor.lastrowid


def hammasi(ulanish):
    qatorlar = ulanish.execute(
        "SELECT nom, summa, turkum, sana FROM xarajat ORDER BY sana, id"
    ).fetchall()
    return [
        {
            "nom": q["nom"],
            "summa": q["summa"],
            "turkum": q["turkum"],
            "sana": date.fromisoformat(q["sana"]),
        }
        for q in qatorlar
    ]
Python
class SmsXabarchi:
    """Haqiqiy xizmat - testda hech qachon chaqirilmasligi kerak."""

    def ogohlantir(self, matn):
        raise RuntimeError("haqiqiy SMS yuborildi")


def oy_yakunini_xabar_qil(xabarchi, jami, chegara, daraja):
    """Faqat sariq va qizil darajada xabar yuboradi."""
    if daraja == "yashil":
        return False
    xabarchi.ogohlantir(
        f"Oylik xarajat {jami} so'm ({chegara} so'mdan {daraja} daraja)"
    )
    return True

Reja #

Piramida bo'yicha yuramiz - pastdan yuqoriga:

Xarajat hisobchisi uchun test rejasi 1. Modul testlari - qoidalar.py Chegara qiymatlari, xato holatlari, parametrlangan jadvallar 2. Dublyor testlari - xabarchi.py Spy bilan: xabar yuborildimi, matni to'g'rimi 3. Integratsiya testlari - ombor.py Haqiqiy SQLite, aylanma yo'l: saqla -> o'qi -> bir xil 4. API testlari - ilova.py Holat kodlari, JSON tanasi, xato yo'llari Har daraja o'zidan pastdagiga tayanadi - shuning uchun tartib shunday
Pastdan yuqoriga: har qadam oldingisiga tayanadi

1-qadam: modul testlari #

Avval xarajat_yarat ning xato yo'llari. Ular bir xil shaklda bo'lgani uchun parametrlaymiz (6-bo'lim):

Python
from datetime import date

import pytest

from qoidalar import xarajat_yarat

SANA = date(2026, 3, 15)


def test_togri_xarajat_yaratiladi():
    xarajat = xarajat_yarat("  Non  ", 12_000, "oziq-ovqat", SANA)

    assert xarajat == {
        "nom": "Non",
        "summa": 12_000,
        "turkum": "oziq-ovqat",
        "sana": SANA,
    }


@pytest.mark.parametrize("nom, summa, turkum, kutilgan_xato", [
    ("", 1000, "uy", "nom bo'sh"),
    ("   ", 1000, "uy", "nom bo'sh"),
    ("Non", 0, "uy", "musbat"),
    ("Non", -5, "uy", "musbat"),
    ("Non", 1000, "sayohat", "noma'lum turkum"),
])
def test_notogri_xarajat_xato_beradi(nom, summa, turkum, kutilgan_xato):
    with pytest.raises(ValueError, match=kutilgan_xato):
        xarajat_yarat(nom, summa, turkum, SANA)


def test_sana_notogri_turda():
    with pytest.raises(TypeError, match="date turida"):
        xarajat_yarat("Non", 1000, "uy", "2026-03-15")
Terminal
pytest -q -p no:randomly test_qoidalar.py
Natija
.......                                                          [100%]
7 passed in 0.01s

Endi eng muhim qism - chegara qiymatlari. ogohlantirish_darajasi da ikkita chegara bor (70% va 100%), demak har biri uchun uchtadan qiymat sinaladi:

Python
import pytest

from qoidalar import ogohlantirish_darajasi

CHEGARA = 1_000_000


@pytest.mark.parametrize("jami, kutilgan", [
    (0, "yashil"),              # eng past
    (699_999, "yashil"),        # sariqdan bir tiyin past
    (700_000, "sariq"),         # aynan 70%
    (700_001, "sariq"),
    (999_999, "sariq"),         # qizildan bir tiyin past
    (1_000_000, "qizil"),       # aynan 100%
    (1_000_001, "qizil"),
    (50_000_000, "qizil"),      # juda katta
])
def test_daraja_chegaralari(jami, kutilgan):
    assert ogohlantirish_darajasi(jami, CHEGARA) == kutilgan


def test_nol_chegara_xato_beradi():
    with pytest.raises(ValueError, match="musbat"):
        ogohlantirish_darajasi(100, 0)
Terminal
pytest -q -p no:randomly test_darajalar.py
Natija
.........                                                        [100%]
9 passed in 0.01s

O'nta holat - va ularning to'rttasi aynan chegarada. Agar kimdir >= ni > ga almashtirsa, test darhol aytadi.

Qolgan ikkita funksiya uchun ham testlar yozamiz. Ularga fixture qo'l keladi (7-bo'lim):

Python
from datetime import date

import pytest

from qoidalar import xarajat_yarat


@pytest.fixture
def xarajat_yasa():
    """Fabrika: testda faqat muhim maydonlar ko'rsatiladi."""
    def yasovchi(nom="Non", summa=10_000, turkum="oziq-ovqat", sana=date(2026, 3, 15)):
        return xarajat_yarat(nom, summa, turkum, sana)
    return yasovchi


@pytest.fixture
def mart_xarajatlari(xarajat_yasa):
    return [
        xarajat_yasa(nom="Non", summa=12_000, sana=date(2026, 3, 1)),
        xarajat_yasa(nom="Metro", summa=2_000, turkum="transport", sana=date(2026, 3, 2)),
        xarajat_yasa(nom="Kommunal", summa=450_000, turkum="uy", sana=date(2026, 3, 5)),
        xarajat_yasa(nom="Sut", summa=18_000, sana=date(2026, 3, 20)),
        xarajat_yasa(nom="Fevral noni", summa=9_000, sana=date(2026, 2, 28)),
    ]
Python
from qoidalar import oylik_jami, turkum_boyicha


def test_bosh_royxat_jamisi_nol():
    assert oylik_jami([], 2026, 3) == 0


def test_faqat_kerakli_oy_hisoblanadi(mart_xarajatlari):
    assert oylik_jami(mart_xarajatlari, 2026, 3) == 482_000


def test_boshqa_oy_alohida(mart_xarajatlari):
    assert oylik_jami(mart_xarajatlari, 2026, 2) == 9_000


def test_mavjud_bolmagan_oy(mart_xarajatlari):
    assert oylik_jami(mart_xarajatlari, 2026, 7) == 0


def test_turkumlar_jamlanadi(mart_xarajatlari):
    assert turkum_boyicha(mart_xarajatlari) == {
        "oziq-ovqat": 39_000,
        "transport": 2_000,
        "uy": 450_000,
    }


def test_bosh_turkumlar_qaytarilmaydi(xarajat_yasa):
    natija = turkum_boyicha([xarajat_yasa(turkum="transport")])

    assert list(natija) == ["transport"]
Terminal
pytest -q -p no:randomly test_jamlash.py
Natija
......                                                           [100%]
6 passed in 0.01s

2-qadam: dublyor bilan xabarnoma #

oy_yakunini_xabar_qil tashqi xizmatni chaqiradi. Uni spy bilan almashtiramiz (8-bo'lim):

Python
import pytest

from xabarchi import oy_yakunini_xabar_qil


class SoxtaXabarchi:
    def __init__(self):
        self.xabarlar = []

    def ogohlantir(self, matn):
        self.xabarlar.append(matn)


@pytest.fixture
def xabarchi():
    return SoxtaXabarchi()


def test_yashil_darajada_xabar_ketmaydi(xabarchi):
    yuborildi = oy_yakunini_xabar_qil(xabarchi, 100_000, 1_000_000, "yashil")

    assert yuborildi is False
    assert xabarchi.xabarlar == []


@pytest.mark.parametrize("daraja", ["sariq", "qizil"])
def test_sariq_va_qizil_darajada_xabar_ketadi(xabarchi, daraja):
    yuborildi = oy_yakunini_xabar_qil(xabarchi, 900_000, 1_000_000, daraja)

    assert yuborildi is True
    assert len(xabarchi.xabarlar) == 1


def test_xabar_matnida_summa_bor(xabarchi):
    oy_yakunini_xabar_qil(xabarchi, 900_000, 1_000_000, "sariq")

    assert "900000" in xabarchi.xabarlar[0]
    assert "sariq" in xabarchi.xabarlar[0]
Terminal
pytest -q -p no:randomly test_xabarchi.py
Natija
....                                                             [100%]
4 passed in 0.01s

Diqqat: SmsXabarchi hech qachon yaratilmadi. Agar kod tasodifan haqiqiy xizmatni chaqirsa, u RuntimeError tashlaydi va test yiqiladi - bu ataylab qo'yilgan tuzoq.

3-qadam: integratsiya testlari #

Endi haqiqiy SQLite bilan (11-bo'lim). Fixture conftest.py ga qo'shiladi:

Python
from datetime import date

import pytest

from ombor import sxemani_yarat, ulanish_och
from qoidalar import xarajat_yarat


@pytest.fixture
def xarajat_yasa():
    """Fabrika: testda faqat muhim maydonlar ko'rsatiladi."""
    def yasovchi(nom="Non", summa=10_000, turkum="oziq-ovqat", sana=date(2026, 3, 15)):
        return xarajat_yarat(nom, summa, turkum, sana)
    return yasovchi


@pytest.fixture
def mart_xarajatlari(xarajat_yasa):
    return [
        xarajat_yasa(nom="Non", summa=12_000, sana=date(2026, 3, 1)),
        xarajat_yasa(nom="Metro", summa=2_000, turkum="transport", sana=date(2026, 3, 2)),
        xarajat_yasa(nom="Kommunal", summa=450_000, turkum="uy", sana=date(2026, 3, 5)),
        xarajat_yasa(nom="Sut", summa=18_000, sana=date(2026, 3, 20)),
        xarajat_yasa(nom="Fevral noni", summa=9_000, sana=date(2026, 2, 28)),
    ]


@pytest.fixture
def ulanish():
    aloqa = ulanish_och(":memory:")
    sxemani_yarat(aloqa)
    yield aloqa
    aloqa.close()
Python
from datetime import date

from ombor import hammasi, saqla
from qoidalar import oylik_jami


def test_yangi_baza_bosh(ulanish):
    assert hammasi(ulanish) == []


def test_saqlangan_xarajat_ozi_qaytadi(ulanish, xarajat_yasa):
    xarajat = xarajat_yasa(nom="Non", summa=12_000)

    saqla(ulanish, xarajat)

    assert hammasi(ulanish) == [xarajat]


def test_sana_turi_saqlanadi(ulanish, xarajat_yasa):
    saqla(ulanish, xarajat_yasa(sana=date(2026, 3, 15)))

    olingan = hammasi(ulanish)[0]

    assert olingan["sana"] == date(2026, 3, 15)
    assert isinstance(olingan["sana"], date)


def test_natija_sana_boyicha_tartiblanadi(ulanish, mart_xarajatlari):
    for xarajat in mart_xarajatlari:
        saqla(ulanish, xarajat)

    sanalar = [x["sana"] for x in hammasi(ulanish)]

    assert sanalar == sorted(sanalar)


def test_bazadan_oqib_jami_hisoblanadi(ulanish, mart_xarajatlari):
    for xarajat in mart_xarajatlari:
        saqla(ulanish, xarajat)

    assert oylik_jami(hammasi(ulanish), 2026, 3) == 482_000
Terminal
pytest -q -p no:randomly test_ombor.py
Natija
.....                                                            [100%]
5 passed in 0.01s

Uchinchi test alohida muhim: SQLite sanani matn sifatida saqlaydi, shuning uchun uni date ga qaytarish kodda qilinadi. Bu aynan modul testi ko'ra olmaydigan xato turi.

4-qadam: API testlari #

Python
from datetime import date

from flask import Flask, jsonify, request

from ombor import hammasi, saqla, sxemani_yarat, ulanish_och
from qoidalar import OYLIK_CHEGARA, ogohlantirish_darajasi, oylik_jami, xarajat_yarat


def ilova_yarat(ulanish=None):
    app = Flask(__name__)
    if ulanish is None:
        ulanish = ulanish_och(":memory:")
        sxemani_yarat(ulanish)
    app.config["ULANISH"] = ulanish

    @app.get("/xarajatlar")
    def royxat():
        xarajatlar = hammasi(app.config["ULANISH"])
        return jsonify([
            {**x, "sana": x["sana"].isoformat()} for x in xarajatlar
        ])

    @app.post("/xarajatlar")
    def qosh():
        malumot = request.get_json(silent=True) or {}
        try:
            xarajat = xarajat_yarat(
                malumot.get("nom", ""),
                malumot.get("summa", 0),
                malumot.get("turkum", ""),
                date.fromisoformat(malumot.get("sana", "")),
            )
        except ValueError as xato:
            return jsonify({"xato": str(xato)}), 400
        yozuv_id = saqla(app.config["ULANISH"], xarajat)
        return jsonify({"id": yozuv_id}), 201

    @app.get("/hisobot/<int:yil>/<int:oy>")
    def hisobot(yil, oy):
        jami = oylik_jami(hammasi(app.config["ULANISH"]), yil, oy)
        return jsonify({
            "yil": yil,
            "oy": oy,
            "jami": jami,
            "daraja": ogohlantirish_darajasi(jami, OYLIK_CHEGARA),
        })

    return app
Terminal
pip install flask
Natija
Collecting flask
  Downloading flask-3.1.3-py3-none-any.whl.metadata (3.2 kB)
Collecting blinker>=1.9.0 (from flask)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting click>=8.1.3 (from flask)
  Downloading click-8.5.0-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous>=2.2.0 (from flask)
  Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting jinja2>=3.1.2 (from flask)
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting markupsafe>=2.1.1 (from flask)
  Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
Collecting werkzeug>=3.1.0 (from flask)
  Downloading werkzeug-3.1.8-py3-none-any.whl.metadata (4.0 kB)
Downloading flask-3.1.3-py3-none-any.whl (103 kB)
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading click-8.5.0-py3-none-any.whl (125 kB)
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
Downloading werkzeug-3.1.8-py3-none-any.whl (226 kB)
Installing collected packages: markupsafe, itsdangerous, click, blinker, werkzeug, jinja2, flask

Successfully installed blinker-1.9.0 click-8.5.0 flask-3.1.3 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.3 werkzeug-3.1.8

Test mijozi uchun fixture qo'shamiz (12-bo'lim):

Python
import pytest

from ilova import ilova_yarat


@pytest.fixture
def mijoz(ulanish):
    app = ilova_yarat(ulanish)
    app.config["TESTING"] = True
    with app.test_client() as c:
        yield c


def test_bosh_royxat(mijoz):
    javob = mijoz.get("/xarajatlar")

    assert javob.status_code == 200
    assert javob.get_json() == []


def test_xarajat_qoshiladi(mijoz):
    javob = mijoz.post("/xarajatlar", json={
        "nom": "Non", "summa": 12_000,
        "turkum": "oziq-ovqat", "sana": "2026-03-01",
    })

    assert javob.status_code == 201
    assert javob.get_json() == {"id": 1}


def test_qoshilgan_xarajat_royxatda(mijoz):
    mijoz.post("/xarajatlar", json={
        "nom": "Non", "summa": 12_000,
        "turkum": "oziq-ovqat", "sana": "2026-03-01",
    })

    javob = mijoz.get("/xarajatlar")

    assert javob.get_json() == [{
        "nom": "Non", "summa": 12_000,
        "turkum": "oziq-ovqat", "sana": "2026-03-01",
    }]


@pytest.mark.parametrize("tana", [
    {},
    {"nom": "", "summa": 1000, "turkum": "uy", "sana": "2026-03-01"},
    {"nom": "Non", "summa": -1, "turkum": "uy", "sana": "2026-03-01"},
    {"nom": "Non", "summa": 1000, "turkum": "sayohat", "sana": "2026-03-01"},
])
def test_notogri_malumot_400_beradi(mijoz, tana):
    javob = mijoz.post("/xarajatlar", json=tana)

    assert javob.status_code == 400
    assert "xato" in javob.get_json()


def test_bosh_oy_hisoboti(mijoz):
    javob = mijoz.get("/hisobot/2026/3")

    assert javob.status_code == 200
    assert javob.get_json() == {"yil": 2026, "oy": 3, "jami": 0, "daraja": "yashil"}


def test_hisobot_darajani_hisoblaydi(mijoz):
    mijoz.post("/xarajatlar", json={
        "nom": "Ijara", "summa": 2_500_000,
        "turkum": "uy", "sana": "2026-03-01",
    })

    javob = mijoz.get("/hisobot/2026/3")

    assert javob.get_json()["jami"] == 2_500_000
    assert javob.get_json()["daraja"] == "sariq"
Terminal
pytest -q -p no:randomly test_ilova.py
Natija
.........                                                        [100%]
9 passed in 0.13s

5-qadam: qoplamani tekshirish #

Hamma testni birga ishga tushirib, qoplamani o'lchaymiz (14-bo'lim):

Terminal
pip install pytest-cov
Natija
Collecting pytest-cov
  Downloading pytest_cov-7.1.0-py3-none-any.whl.metadata (32 kB)
Collecting coverage>=7.10.6 (from coverage[toml]>=7.10.6->pytest-cov)
  Downloading coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (8.2 kB)
Requirement already satisfied: pluggy>=1.2 in ./.venv/lib/python3.13/site-packages (from pytest-cov) (1.6.0)
Requirement already satisfied: pytest>=7 in ./.venv/lib/python3.13/site-packages (from pytest-cov) (9.1.1)
Requirement already satisfied: iniconfig>=1.0.1 in ./.venv/lib/python3.13/site-packages (from pytest>=7->pytest-cov) (2.3.0)
Requirement already satisfied: packaging>=22 in ./.venv/lib/python3.13/site-packages (from pytest>=7->pytest-cov) (26.3)
Requirement already satisfied: pygments>=2.7.2 in ./.venv/lib/python3.13/site-packages (from pytest>=7->pytest-cov) (2.21.0)
Downloading pytest_cov-7.1.0-py3-none-any.whl (22 kB)
Downloading coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (257 kB)
Installing collected packages: coverage, pytest-cov

Successfully installed coverage-7.16.1 pytest-cov-7.1.0
INI
[run]
branch = True
omit =
    */.venv/*
    test_*.py
    conftest.py
Terminal
pytest -q -p no:randomly --cov=. --cov-report=term-missing
Natija
........................................                         [100%]
============================ tests coverage ============================
___________ coverage: platform linux, python 3.13.12-final-0 ___________

Name          Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------
ilova.py         28      2      2      1    90%   12-13
ombor.py         16      0      0      0   100%
qoidalar.py      29      0     16      0   100%
xabarchi.py       8      1      2      0    90%   5
---------------------------------------------------------
TOTAL            81      3     20      1    96%
40 passed in 0.28s

Ikkita teshik ko'rindi:

  • xabarchi.py:5 - SmsXabarchi.ogohlantir. U hech qachon chaqirilmagan va bu to'g'ri: biz uni ataylab chaqirmayapmiz.
  • ilova.py:12-13 - ilova_yarat ga ulanish berilmagan hol. Testda biz har doim o'z ulanishimizni beramiz, shuning uchun sukut yo'l sinalmagan.

Ikkinchisi haqiqiy teshik: dastur ishga tushganda aynan shu qatorlar bajariladi. Uni yopish uchun bitta test yetadi - keyingi topshiriqda shuni yozasiz.

Birinchisini esa qoplamadan chiqarish to'g'riroq.

Bunday qatorlarni pragma: no cover bilan belgilash mumkin:

Python
class SmsXabarchi:
    """Haqiqiy xizmat - testda hech qachon chaqirilmasligi kerak."""

    def ogohlantir(self, matn):   # pragma: no cover
        raise RuntimeError("haqiqiy SMS yuborildi")


def oy_yakunini_xabar_qil(xabarchi, jami, chegara, daraja):
    """Faqat sariq va qizil darajada xabar yuboradi."""
    if daraja == "yashil":
        return False
    xabarchi.ogohlantir(
        f"Oylik xarajat {jami} so'm ({chegara} so'mdan {daraja} daraja)"
    )
    return True
INI
[run]
branch = True
omit =
    */.venv/*
    test_*.py
    conftest.py

[report]
exclude_lines =
    pragma: no cover
    if __name__ == .__main__.:
Terminal
pytest -q -p no:randomly --cov=. --cov-report=term-missing
Natija
........................................                         [100%]
============================ tests coverage ============================
___________ coverage: platform linux, python 3.13.12-final-0 ___________

Name          Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------
ilova.py         28      2      2      1    90%   12-13
ombor.py         16      0      0      0   100%
qoidalar.py      29      0     16      0   100%
xabarchi.py       6      0      2      0   100%
---------------------------------------------------------
TOTAL            79      2     20      1    97%
40 passed in 0.27s
pragma: no cover - ehtiyotkorlik bilan

Bu belgi qatorni qoplamadan chiqaradi, ya'ni hisobotni go'zallashtiradi. Uni faqat haqiqatan testlanmasligi kerak bo'lgan kodga qo'ying:

  • ishlab chiqarishdagi tashqi xizmat chaqiruvi;
  • if __name__ == "__main__" bloki;
  • himoya uchun yozilgan, hech qachon yuz bermasligi kerak bo'lgan tarmoq.

Agar uni "test yozishga vaqtim yo'q" degan sabab bilan qo'ysangiz, qoplama hisoboti yolg'on gapira boshlaydi.

6-qadam: belgilar va CI #

Oxirgi qadam - to'plamni quvurga ulash (19-bo'lim):

INI
[pytest]
addopts = -ra --strict-markers
markers =
    integratsiya: ma'lumotlar bazasi talab qiladi
    sekin: sekin ishlaydigan testlar

--strict-markers - e'lon qilinmagan belgi ishlatilsa, pytest xato beradi. Bu imlo xatosidan himoya qiladi.

YAML
name: Testlar

on: [push, pull_request]

jobs:
  testlar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
          cache: pip
      - run: pip install pytest pytest-cov flask
      - run: pytest --cov=. --cov-report=term-missing --junitxml=natija.xml
      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: natija
          path: natija.xml

To'plamning yakuniy holati:

Terminal
pytest -p no:randomly --collect-only -q | tail -3
Natija
test_xabarchi.py::test_xabar_matnida_summa_bor

40 tests collected in 0.09s
Terminal
pytest -q -p no:randomly
Natija
........................................                         [100%]
40 passed in 0.15s

Nimaga erishdik #

DarajaTestlarNimani qo'riqlaydi
ModulQoidalar, chegaralar, xato holatlariHisob-kitob mantig'i
DublyorXabarnoma shartlariTashqi xizmat chaqiruvi
IntegratsiyaSQLite bilan aylanma yo'lSQL va tur o'girish
APIHolat kodlari, JSON, xatolarShartnoma

Va eng muhimi - bu to'plam tez. U har kommitda ishlaydi, demak u haqiqatan foyda beradi.

Loyihangizda qayerdan boshlash kerak

Testi yo'q loyihaga bir kunda to'liq to'plam qurib bo'lmaydi. Tartib shunday:

  1. Eng qimmat mantiqdan boshlang - pul, ruxsat, hisob-kitob.
  2. Har nosozlikdan oldin uni ko'rsatadigan test yozing (1-bo'lim).
  3. Yangi kod testsiz kirmasin - bu eng arzon qoida.
  4. Qoplamaning tushishini kuzating, darajasini emas.
  5. Eski kodga xarakteristik test bilan yaqinlashing (16-bo'lim).

Bir necha oydan keyin to'plam o'z-o'zidan paydo bo'ladi.

Amaliy topshiriq
  1. Uchta modulni yozib, modul testlarini ishga tushiring.
  2. ogohlantirish_darajasi da >= ni > ga almashtiring - nechta test yiqildi?
  3. Yangi turkum qo'shing va unga test yozing.
  4. Dublyor testlariga uchinchi holat qo'shing: chegara aynan 100%.
  5. Integratsiya testlarida ORDER BY ni olib tashlang - qaysi test yiqiladi?
  6. API ga DELETE /xarajatlar/<id> qo'shing va unga to'rtta test yozing.
  7. ilova_yarat() ni ulanishsiz chaqiradigan test yozib, qolgan teshikni yoping.
  8. --cov-branch ni yoqib, farqni solishtiring.
  9. Integratsiya testlariga integratsiya belgisini qo'ying va -m "not integratsiya" ni sinang.
  10. O'z loyihangizni tanlab, shu tartibda birinchi beshta testni yozing.

Xulosa #

  • To'plam piramida bo'yicha pastdan yuqoriga quriladi: modul, dublyor, integratsiya, API.
  • Bir xil shakldagi xato holatlari parametrize bilan bitta testga jamlanadi.
  • Ikkita chegarasi bor funksiyaga oltitadan kam bo'lmagan chegara testi kerak.
  • Fabrika fixture testda faqat muhim maydonni ko'rsatish imkonini beradi.
  • Haqiqiy tashqi xizmat testda chaqirilsa istisno tashlasin - bu ataylab qo'yilgan tuzoq.
  • SQLite sanani matn sifatida saqlaydi; tur o'girishni faqat integratsiya testi tekshiradi.
  • Bo'sh baza va bo'sh ro'yxat testlari izolyatsiya ishlayotganini tasdiqlaydi.
  • API testida holat kodi ham, tananing butun tarkibi ham tekshiriladi.
  • Ilova bog'liqlikni (ulanishni) tashqaridan olsa, uni testda almashtirish oson bo'ladi.
  • .coveragerc sozlamalarni bir joyda saqlaydi va test fayllarini hisobdan chiqaradi.
  • pragma: no cover faqat haqiqatan testlanmasligi kerak bo'lgan kodga qo'yiladi.
  • --strict-markers e'lon qilinmagan belgidagi imlo xatosidan himoya qiladi.
  • Tez to'plam har kommitda ishlaydi - va faqat shunda u haqiqatan foyda beradi.
  • Mavjud loyihada eng qimmat mantiqdan boshlang va yangi kodni testsiz kiritmang.
  • Bir necha oy davomida shu qoidaga amal qilsangiz, to'plam o'z-o'zidan paydo bo'ladi.

Darslik shu yerda tugadi. Endi eng muhim qadam qoldi: o'z loyihangizni ochib, birinchi testni yozing.

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.