Asosiy kontentga o‘tish

Maqolalar 8 daqiqa 408 ko‘rish

pytest-django: birinchi testni yozamiz

Kod ishlayaptimi — brauzerda bosib ko'rish bilan tekshirib yurgan bo'lsangiz, bu maqola siz uchun. pytest-django'ni sozlaymiz, birinchi testni yozamiz va «o'zgartirishdan qo'rqmaslik» degan tuyg'u qayerdan kelishini ko'ramiz.

testing django python

Mundarija

Kod yozdingiz, brauzerda ochib bosib ko'rdingiz — ishlayapti. Bir haftadan keyin boshqa joyini o'zgartirdingiz va bilmagan holda avvalgi ishlagan joyni buzdingiz. Buni foydalanuvchi topib beradi. Tanish holatmi? Men ham shu yo'ldan o'tganman va testlarga jiddiy qarashim aynan ToDo loyihasidan boshlangan — uni ataylab "test intizomini o'rganish" maqsadida qurganman.

Testning ma'nosini bitta gap bilan aytish mumkin: bosib ko'rishni kod qilib yozib qo'yish. Bir marta yozasiz, keyin har o'zgarishdan so'ng kompyuter siz uchun ming marta bosib ko'radi.

Nega aynan pytest#

Django'ning o'z test tizimi ham bor, u unittest asosida ishlaydi. Lekin Python dunyosida amalda standartga aylangan vosita — pytest. Unda assert yozish oddiy, xato chiqqanda terminaldagi xabar tushunarli, fixture tizimi esa kuchli. Uni Django bilan bog'lash uchun bitta plagin yetadi:

Terminal
uv add --dev pytest pytest-django

Sozlama ham bitta fayl — pytest'ga Django sozlamalarini qayerdan olishni aytamiz:

INI
# pytest.ini (loyiha ildizida)
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = test_*.py

Loyiha va birinchi test#

Misol uchun kichik loyiha yaratamiz (Django loyihangiz allaqachon bo'lsa, faqat apps/todos qismini qo'shing):

Terminal
uv add django
uv run django-admin startproject config .
mkdir -p apps/todos/tests
touch apps/__init__.py apps/todos/__init__.py apps/todos/tests/__init__.py
Python
# config/settings.py
INSTALLED_APPS = [
    # ...
    "apps.todos",
]

INSTALLED_APPS dagi qatorni unutmang: busiz pytest testni ishga tushirishga ham ulgurmaydi — fayllarni yig'ish bosqichidayoq RuntimeError: Model class apps.todos.models.Task doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS xatosi chiqadi.

Misolimiz — ToDo ilovasining soddalashtirilgan modeli:

Python
# apps/todos/models.py
from django.db import models
from django.utils import timezone


class Task(models.Model):
    title = models.CharField(max_length=200)
    is_done = models.BooleanField(default=False)
    due_date = models.DateField(null=True, blank=True)

    @property
    def is_overdue(self) -> bool:
        # muddat o'tgan va hali bajarilmagan — kechikkan
        if self.due_date is None or self.is_done:
            return False
        return self.due_date < timezone.localdate()

is_overdue — test yozishni boshlash uchun ideal nomzod: kichkina, lekin ichida uchta shart va bitta nozik chegara yashiringan mantiq. Test fayli model bilan yonma-yon turadi:

Fayl tuzilishi
apps/todos/
├── models.py
└── tests/
    ├── __init__.py
    └── test_models.py    ← birinchi testimiz shu yerda
Python
# apps/todos/tests/test_models.py
from datetime import timedelta

import pytest
from django.utils import timezone

from apps.todos.models import Task

# bugun va kecha — testlarda qayta-qayta kerak bo'ladi
BUGUN = timezone.localdate()
KECHA = BUGUN - timedelta(days=1)


@pytest.mark.django_db
def test_muddati_otgan_vazifa_kechikkan():
    task = Task.objects.create(title="Hisobot", due_date=KECHA)
    assert task.is_overdue


@pytest.mark.django_db
def test_bajarilgan_vazifa_kechikkan_hisoblanmaydi():
    task = Task.objects.create(title="Hisobot", due_date=KECHA, is_done=True)
    assert not task.is_overdue


@pytest.mark.django_db
def test_muddatsiz_vazifa_kechikmaydi():
    task = Task.objects.create(title="Kitob o'qish")
    assert not task.is_overdue


@pytest.mark.django_db
def test_muddati_bugun_bolgan_vazifa_hali_kechikmagan():
    # chegaraviy holat: muddat bugun tugaydi — kun hali tugamagan
    task = Task.objects.create(title="Hisobot", due_date=BUGUN)
    assert not task.is_overdue

Ishga tushiramiz:

Terminal
uv run pytest
# ...
# 4 passed in 0.05s

Uch nuqtaga e'tibor qarataman. Birinchisi — @pytest.mark.django_db belgisi: usiz test bazaga tegolmaydi. Bu ataylab qilingan, chunki bazasiz test yashin tezligida yuradi va pytest sizni "bazani faqat kerak joyda ishlat" deb tarbiyalaydi. Ikkinchisi — test nomlari o'zbekcha va gap kabi o'qiladi: test yiqilganda nomining o'zi nima buzilganini aytib turadi. Uchinchisi — har test bitta narsani tekshiradi: to'rt holatga to'rtta alohida test, bitta katta test emas.

To'rtinchi test — chegaraviy holat, va u eng qimmatlisi. < o'rniga tasodifan <= yozilsa, muddati bugun tugaydigan vazifa ertalabdanoq "kechikkan" bo'lib ko'rinadi. Muddati o'tgan, bajarilgan va muddatsiz holatlar uchun yozilgan uchta test bu xatoni sezmaydi — sinovda ular <= bilan ham yashil qoldi. Faqat due_date=BUGUN testi qizarib, xatoni ushladi.

Test bazasi haqida xotirjam bo'ling#

Boshlovchilarni ko'p tashvishlantiradigan savol: "testlar mening bazamni buzib qo'ymaydimi?" Yo'q. pytest-django har test seansi uchun alohida, vaqtinchalik baza yaratadi (nomi odatda test_ bilan boshlanadi), ishini o'sha yerda qiladi va oxirida o'chirib tashlaydi. Har test esa tranzaksiya ichida yuradi va tugagach orqaga qaytariladi — ya'ni testlar bir-birining ma'lumotini ham ko'rmaydi. Siz Task.objects.create bilan nima yaratmang, haqiqiy bazangizga bitta yozuv ham tushmaydi.

Yiqilgan test — bu yaxshi test#

Endi eng muhim mashqni qilamiz: mantiqni ataylab buzamiz. is_overduedagi self.is_done shartini olib tashlab, testlarni qayta ishga tushiring:

Natija
________________ test_bajarilgan_vazifa_kechikkan_hisoblanmaydi ________________

    @pytest.mark.django_db
    def test_bajarilgan_vazifa_kechikkan_hisoblanmaydi():
        task = Task.objects.create(title="Hisobot", due_date=KECHA, is_done=True)
>       assert not task.is_overdue
E       assert not True
E        +  where True = <Task: Task object (1)>.is_overdue

apps/todos/tests/test_models.py:22: AssertionError
=========================== short test summary info ============================
FAILED apps/todos/tests/test_models.py::test_bajarilgan_vazifa_kechikkan_hisoblanmaydi
========================= 1 failed, 3 passed in 0.06s ==========================

pytest qaysi test, qaysi qatorda va qaysi qiymat sabab yiqilganini aniq ko'rsatadi: > belgisi yiqilgan qatorga ishora qiladi, where True = ... esa is_overdue aynan True qaytarganini aytadi.

Mana shu qizil satr — testlarning butun qiymati. Siz xatoni foydalanuvchidan emas, terminaldan, o'zgarish qilgan daqiqangizda eshitdingiz. Endi shartni joyiga qaytaring — yashil. Bu tsiklni his qilgan odam testsiz loyihaga qaytishi qiyin.

Testni qachon yozish kerak?

Ideal javob "koddan oldin" (TDD), lekin halol boshlanish — "kod bilan birga": yangi mantiq yozdingizmi, o'sha o'tirishning o'zida 2-3 test ham yozing. Eng foydali joylar — shartli mantiq (is_overdue kabi), hisob-kitoblar va chegaraviy holatlar. Getter-setter'larga test yozib vaqt ketkazmang.

Yakun#

pytest-django'ni ishga tushirish ikki qadam edi: plagin va pytest.ini. Undan keyingisi — odat masalasi: har mantiqqa uch-to'rt satr test, nomlari gap kabi, har biri bitta narsani tekshiradi. Baza haqida qayg'urmaysiz — u vaqtinchalik va sizniki emas.

Keyingi qismda ko'lamni kengaytiramiz: unit va integration testlar farqi — "kichik va tez" bilan "katta va ishonchli" o'rtasidagi muvozanatni qanday tanlash.

8 daqiqa 408 marta o‘qildi