Asosiy kontentga o‘tish

Maqolalar 11 daqiqa 873 ko‘rish

DRF API'ni APIClient bilan testlash: status, JSON va ruxsatlar

API'ni Postman'da qo'lda bosib tekshirish ham 'brauzerda bosib ko'rish'ning o'zi. DRF'ning APIClient'i bilan endpoint'larni kod orqali tekshiramiz: to'g'ri javoblar, validatsiya xatolari va kirish ruxsatlari — ToDo API misolida.

testing api django python

Mundarija

Seriyaning birinchi va ikkinchi qismlarida oddiy Django view'largacha yetib keldik. Endi navbat API'da. REST API yozgan odamning odatiy ish tartibi tanish: kod yozdi, Postman'ni ochdi, so'rov yubordi, javobga qaradi. Bu ham aslida "brauzerda bosib ko'rish"ning o'zi — faqat tool boshqa. Va davosi ham o'sha: bosishni kod qilib yozib qo'yamiz.

Misol sifatida ToDo loyihasidagi API'ning soddalashtirilgan ko'rinishini olamiz. API har foydalanuvchiga faqat o'z vazifalarini ko'rsatadi, shuning uchun modelga egasi qo'shiladi:

Python
# apps/todos/models.py
from django.conf import settings
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)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="tasks",
    )

    @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()

null=True bu yerda ataylab: oldingi qismlardagi testlar vazifani egasiz yaratadi (Task.objects.create(title=...)), majburiy maydon esa ularning hammasini IntegrityError bilan sindirardi. Egasiz vazifa API'da hech kimga ko'rinmaydi — pastdagi get_queryset shunga kafolat beradi.

Terminal
uv add djangorestframework
uv run python manage.py makemigrations todos

INSTALLED_APPS ga "rest_framework" qo'shiladi, API esa router orqali ulanadi:

Python
# config/urls.py
from django.contrib import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from apps.todos.api import TaskViewSet

router = DefaultRouter()
router.register("tasks", TaskViewSet, basename="task")

urlpatterns = [
    path("admin/", admin.site.urls),
    path("todos/", include("apps.todos.urls")),
    path("api/", include(router.urls)),
]
Python
# apps/todos/serializers.py
from rest_framework import serializers

from apps.todos.models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ["id", "title", "is_done", "due_date"]
Python
# apps/todos/api.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from apps.todos.models import Task
from apps.todos.serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]  # API faqat kirgan foydalanuvchiga

    def get_queryset(self):
        # har kim faqat o'z vazifalarini ko'radi
        return Task.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

APIClient: oddiy client'dan nimasi ortiq#

pytest-django bergan client fixture'i HTML sahifalar uchun qulay edi. API uchun DRF o'zining APIClientini beradi: JSON'ni o'zi seriyalaydi, format="json" bilan Content-Type'ni to'g'rilaydi va eng foydalisi — force_authenticate orqali login jarayonisiz "kirgan foydalanuvchi" holatini beradi. Uni bir marta fixture qilib olamiz, hamma test foydalanadi:

Python
# apps/todos/tests/conftest.py
import pytest
from django.contrib.auth.models import User
from rest_framework.test import APIClient


@pytest.fixture
def user(db):
    return User.objects.create_user("olim", password="sinov12345")


@pytest.fixture
def api(user):
    """Login qilgan holdagi API mijoz — testlar tayyor holda oladi."""
    client = APIClient()
    # sessiya/token bilan ovora bo'lmaymiz: autentifikatsiyani testlash
    # alohida mavzu, bu yerda esa API mantig'ini tekshiryapmiz
    client.force_authenticate(user=user)
    return client

conftest.py — pytest'ning umumiy fixture ombori: shu papkadagi barcha testlar user va apini parametr sifatida so'rab olaveradi.

To'rt asosiy stsenariy#

API testlari odatda to'rt savol atrofida aylanadi: to'g'ri so'rov to'g'ri javob oladimi, yaroqsiz so'rov to'g'ri rad etiladimi, begona kirolmaydimi va ma'lumot chegarasi buzilmaydimi. Oxirgisi eng muhimi, shuning uchun unga ikkita test yozamiz:

Python
# apps/todos/tests/test_api.py
from django.contrib.auth.models import User
from rest_framework.test import APIClient

from apps.todos.models import Task


def test_vazifa_yaratiladi(api):
    response = api.post("/api/tasks/", {"title": "Maqola yozish"}, format="json")

    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Maqola yozish"
    assert data["is_done"] is False  # default qiymat ham javobda to'g'ri
    assert Task.objects.count() == 1  # bazaga haqiqatan yozildi


def test_bosh_sarlavha_rad_etiladi(api):
    response = api.post("/api/tasks/", {"title": ""}, format="json")

    assert response.status_code == 400
    # xato aynan qaysi maydonda ekani ham javobda aytilgan bo'lishi kerak —
    # mobil/frontend jamoasi shu maydonga qarab xabar ko'rsatadi
    assert "title" in response.json()
    assert Task.objects.count() == 0


def test_kirmagan_foydalanuvchi_rad_etiladi(db):
    anonim = APIClient()  # force_authenticate YO'Q
    response = anonim.get("/api/tasks/")
    # DRF standart sozlamasida 403; token/JWT autentifikatsiyada 401 bo'ladi
    assert response.status_code == 403


def test_begona_vazifa_royxatda_korinmaydi(api):
    begona = User.objects.create_user("g'ani")
    Task.objects.create(title="Begonaning ishi", owner=begona)

    response = api.get("/api/tasks/")

    assert response.status_code == 200
    assert response.json() == []  # ro'yxat bo'sh — begonaniki chiqmadi


def test_begona_vazifani_ozgartirib_ham_ochirib_ham_bolmaydi(api):
    begona = User.objects.create_user("g'ani")
    task = Task.objects.create(title="Begonaning ishi", owner=begona)

    patch = api.patch(f"/api/tasks/{task.id}/", {"title": "buzildi"}, format="json")
    delete = api.delete(f"/api/tasks/{task.id}/")

    # 404, 403 emas: begona vazifa "taqiqlangan" emas, umuman "yo'q" —
    # uning mavjudligini ham oshkor qilmaymiz
    assert patch.status_code == 404
    assert delete.status_code == 404
    task.refresh_from_db()
    assert task.title == "Begonaning ishi"

Anonim foydalanuvchi testida 401 emas, 403 kutilganiga e'tibor bering. DRF 401 ni faqat birinchi autentifikatsiya klassi brauzerga "qanday kirish kerak"ligini aytadigan WWW-Authenticate sarlavhasini bera olsa qaytaradi (Basic, Token, JWT). Standart sozlamada birinchisi SessionAuthentication, u bunday sarlavha bermaydi — javob 403. Sinovda 401 kutgan test aynan shu sabab yiqildi. Loyihangizda token yoki JWT autentifikatsiya birinchi tursa, testda 401 yozasiz.

Oxirgi ikki test alohida e'tiborga loyiq. get_querysetdagi filter(owner=...) qatori — xavfsizlik chegarasi, va bunday chegara har doim testda mustahkamlab qo'yilishi kerak. Ro'yxatda begona vazifa ko'rinmasligi yetarli emas: haqiqiy hujum odatda ID bo'yicha keladi — PATCH /api/tasks/17/ bilan boshqa odamning vazifasini o'zgartirish yoki o'chirish (IDOR). Shuning uchun beshinchi test aynan shuni tekshiradi va 404 kutadi: begona vazifa "taqiqlangan" emas, umuman "yo'q" — uning mavjudligi ham oshkor qilinmaydi.

Tekshirib ko'rdim: get_queryset dagi filtrni olib tashlasam, ikkala test ham darhol qizaradi. Ertaga kimdir refactoring paytida o'sha filtrni yo'qotib qo'ysa, xato jimgina o'tib ketmaydi. API'dagi bunday "meniki-seniki" xatolari eng ko'p uchraydigan zaifliklardan biri, bu haqda OWASP maqolasida ham yozganman.

Yiqilganda nimani o'qiysiz#

Test yiqilsa, pytest javobning o'zini ham ko'rsatadi — lekin unga bitta yordam qo'shsangiz, hayot yana osonlashadi:

Python
    assert response.status_code == 201, response.content

Assert'ning ikkinchi argumenti xato xabariga qo'shiladi: status kutilgancha chiqmasa, terminalda serializer'ning to'liq xato javobini ko'rasiz — "400 keldi" degan quruq faktni emas, aynan nima noto'g'ri ekanini. response.json() emas, response.content yozilgani bejiz emas: URL xato bo'lsa, Django JSON emas, HTML sahifa qaytaradi va json() o'zi ValueError bilan yiqilib, asl sababni yashiradi — sinovda shunday bo'ldi.

Nomlash bilan hujjat

E'tibor bergan bo'lsangiz, test nomlari yana gap kabi: yaratiladi, rad etiladi, ko'rinmaydi. To'plam kattalashganda pytest --collect-only chiqishi o'z-o'zidan API'ning xulq-atvor hujjatiga aylanadi — qaysi holatda nima bo'lishi ro'yxat bo'lib turadi.

Yakun — va seriya xulosasi#

APIClient bilan test yozish oddiy client'dan deyarli farq qilmaydi, lekin API dunyosiga mos uch odat qo'shiladi: statusni tekshirish, JSON tanasini tekshirish va ruxsat chegaralarini tekshirish. Uchinchisi eng muhimi — funksionallik buzilsa foydalanuvchi shikoyat qiladi, xavfsizlik buzilsa hech kim indamaydi.

Uch qismlik testlash seriyasi shu yerda yakunlanadi: birinchi testdan boshladik, unit va integration muvozanatini ko'rdik, API'ni qamrab oldik. Bu bilimlarning jonli namunasi — ToDo loyihasi, test intizomini o'rganish uchun ataylab qurilgan maydon.

11 daqiqa 873 marta o‘qildi