Django Rest Framework – testowanie API

W tym wpisie opiszę testowanie API dla utworzonego kastomowego użytkownika. (wcześniejsze wpisy – >>część 1<< oraz >>część 2<<)

Dostęp do widoku ContactUserCreateRetrieveViewSet umożliwiającego tworzenie nowego użytkownika oraz wyświetlanie konkretnego użytkownika jest zarejestrowany w pliku urls.py aplikacji users tzn.

from rest_framework.routers import DefaultRouter
from .views import ContractUserCreateRetrieveViewSet

router = DefaultRouter()

router.register("", ContractUserCreateRetrieveViewSet, basename="user")

urlpatterns = router.urls

Jako parametr basename zdefiniowałem jako user, do której to nazwy będę się odwoływał podczas testów.

Plik urls.py aplikacji users jest następnie importowany w pliku urls.py projektu tzn.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls")),
    path("api/user/", include("users.urls")),
]

Kompletny punkt dostępowy do tworzenia użytkowników będzie więc miał postać /api/user/

Tworzenie użytkownika i wyświetlanie konkretnego użytkownika będzie realizowane przez konkretne metody z pliku views.py aplikacji users tzn. tworzenie – funkcja create(), wyświetlanie użytkownika – funkcja retrieve()

from django.shortcuts import get_object_or_404
from rest_framework import viewsets, mixins
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import ContractUser
from .serializers import ContractUserSerializer

class ContractUserCreateRetrieveViewSet(
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    viewsets.GenericViewSet,
):
    serializer_class = ContractUserSerializer
    queryset = ContractUser.objects.all()

    def retrieve(self, request, pk):
        permission_classes = [IsAuthenticated]
        queryset = ContractUser.objects.filter(id=request.user.id)
        user = get_object_or_404(queryset, pk=pk)
        serializer = ContractUserSerializer(user)
        return Response(serializer.data)

    def create(self, request):
        serializer = ContractUserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Klasa ContractUserCreateRetrieveViewSet dziedziczy po podstawowej klasie GenericViewSet oraz po klasach mixins, które dodają wymagane funkcjonalności – tworzenie i wyświetlanie użytkownika.

Funkcja create() serializuje podane dane i po sprawdzeniu, czy są one poprawne zapisuje je w bazie i zwraca obiekt Response i odpowiedni kod statusu.

Funkcja retrieve() sprawdza czy użytkownik jest zalogowany i czy filtruje użytkowników, aby zwracany queryset zawierał tylko tego użytkownika, który jest obecnie zalogowany, a następnie zwraca obiekt Response i odpowiedni kod statusu.

W pliku signals.py aplikacji users modyfikuję pole email udostępnione przez klasę AbstractUser, aby pole to było wymagane podczas tworzenia modelu ContractUser tzn.

from .models import ContractUser
from django.dispatch import receiver


@receiver(pre_save, sender=ContractUser)
def create_inactive_user(sender, instance, **kwargs):
    instance.is_active = False
    instance._meta.get_field("email").blank = False
    instance._meta.get_field("email").null = False

Jeśli chcielibyśmy, aby pole email było unikalne, to możemy ustawić parametr unique jako True, tzn.

    instance._meta.get_field("email")._unique = True

Wówczas jeśli użytkownik wpisze email, który już jest w bazie to wystąpi błąd walidacji. Niestety dzięki temu będzie wiadomo, że użytkownik o takim adresie email jest już obecny w bazie.

Aby przetestować tworzenie i wyświetlanie użytkownika tworzę katalog tests w aplikacji users.

Do niego przenoszę plik tests.py utworzony w poprzednim artykule, który zawierał testy modelu i zmieniam mu nazwę na test_model.py

W utworzonym katalogu tests tworzę nowy plik o nazwie test_api.py, który będzie zawierał testy api dla aplikacji users tzn.

from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from ..models import ContractUser


class ContractUserApiTestCase(APITestCase):
    """Testing ContractUser API"""

    def test_create_user(self):
        endpoint = reverse("user-list")
        user_data = {
            "username": "user_1",
            "password": "123456789",
            "email": "user_1@company.com",
        }
        response = self.client.post(endpoint, data=user_data, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_retrieve_user(self):
        user = ContractUser.objects.create(
            username="user1", password="12345", email="user1@company.com"
        )
        self.client.force_authenticate(user)
        endpoint = reverse("user-detail", args=[user.id])
        response = self.client.get(endpoint, format="json")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

W metodzie test_create_user() tworzę endpoint przy użyciu funkcji reverse(). Funkcja reverse() bierze jako parametr basename nadany w pliku urls.py wraz z sufiksem -list. Następnie tworzę dane dla testowego użytkownika i przy wykorzystaniu domyślnego klienta obecnego w klasie APITestCase dokonuję zapytania przy użyciu metody POST. Asercja sprawdza, czy kod statusu odebranej odpowiedzi to 201, gdy pomyślnie utworzono użytkownika, czy też wynosi on 400 w przeciwnym przypadku.

W metodzie test_retrieve_user() tworzę testowego użytkownika i następnie zalogowuję go za pomocą funkcji force_authenticate(). Funkcja reverse() jako parametr bierze wartość basename nadany w pliku urls.py wraz z sufiksem -detail. Jako dodatkowy parametr załączam id zalogowanego użytkownika. Asercja sprawdza, czy kod statusu odpowiedzi to 200, gdy zapytanie zakończyło się sukcesem czy też status wynosi 400 w przeciwnym przypadku.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

six + one =