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.
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:
# 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.
uv add djangorestframework
uv run python manage.py makemigrations todos
INSTALLED_APPS ga "rest_framework" qo'shiladi, API esa router orqali
ulanadi:
# 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)),
]
# 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"]
# 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:
# 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:
# 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:
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.