Testowanie w Django REST framework¶
Aby przetestować działanie naszego API najprościej jest wykorzystać testu funkcjonalne. Jednak warto również przetestować działanie naszych serializatorów (jeśli posiadają nie standardową logikę) lub poszczególnych metody wykorzystywanych w klasach APIView lub ViewSet.
Sprawdzamy czy jest zwracana poprawna odpowiedź poprzez klienta, są to testy funkcjonalne wykorzystujące klienta WebTest.
class ProductAPITests:
def test_can_get_product_details(self, django_app, product_factory):
product = product_factory()
response = django_app.get(f'/products/{product.id}/')
assert response.status_code == 200
assert response.data == ProductSerializer(instance=product).data
def test_can_delete_product(self, django_app, product_factory):
product = product_factory()
response = django_app.delete(f'/products/{product.id}/delete/')
assert response.status_code == 204
assert Product.objects.count() == 0
def test_can_update_product(self, django_app, product_factory):
product = product_factory()
response = django_app.patch_json(f'/products/{product.id}/update/', params={'name': 'Samsung Watch'})
product.refresh_from_db()
self.assertEqual(response.status_code, 200)
self.assertEqual(product.name, 'Samsung Watch')
Testowanie widoków poprzez APIRequestFactory¶
Możemy również napisać testy, tylko i wyłącznie dla konkretnego widoku z pominięciem całego stosu zapytania i odpowiedzi (pomijając np. middleware, czy router dla adresu url).
from rest_framework import status, viewsets
class UserContractViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = UserContractSerializer
def get_queryset(self):
if self.request.user.is_staff:
return UserContract.objects.all()
return UserContract.objects.filter(user=self.request.user)
@pytest.mark.django_db
def test_user_can_see_own_contracts(api_rf, user_factory, user_contract_factory):
view = UserContractViewSet.as_view({'get': 'list'})
user = user_factory()
user_contracts = user_contract_factory.build_batch(3, user=user)
request = api_rf.get('/user_contracts/')
force_authenticate(request, user=user)
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data.get('count') == 3
assert response.data.get("results") == UserContractSerializer(
user_contracts, many=True).data
@pytest.mark.django_db
def test_user_can_see_own_single_contract(api_rf, user_factory, user_contract_factory):
view = UserContractViewSet.as_view({'get': 'retrieve'})
user = user_factory()
user_contract = user_contract_factory(user=user)
request = api_rf.get(f'user_contracts/{user_contract.pk}')
force_authenticate(request, user=user)
response = view(request, pk=user_contract.pk)
assert response.status_code == status.HTTP_200_OK
assert response.data == UserContractSerializer(user_contract).data
Testowanie dekoratora actions¶
Bardzo często korzystając z ViewSet można stworzyć dodatkowe metody wywoływane poprzez url dla obiektu lub listy obiektów. Warto również je przetestować wywołując odpowiednio api widoku.
class UserViewSet(ModelViewSet):
...
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
@pytest.mark.django_db
def test_user_set_password(api_rf, user_factory):
view = UserViewSet.as_view({'get': 'set_password'})
user = user_factory()
request = api_rf.post_json(
f'/user_contracts/{user_contract.pk}/set_password',
params={'password': '123123'})
force_authenticate(request, user=user)
response = view(request, pk=user.pk)
assert response.status_code == status.HTTP_200_OK
Testowanie routera oraz url¶
Widoki funkcyjne¶
from chat.views import get_chats
found = resolve(reverse('referrals'))
assert found2.func.__name__ == get_chats.__name__
Widoki klasowe¶
def test_check_if_recent_url_exist_and_have_good_class(self):
found = resolve('/notifications/recent/')
assert found.func.cls == views.UserNotification
Przykłady ViewSet dla wybranych akcji¶
router = DefaultRouter()
router.register(r'my-list', MyViewSet, base_name="my_list")
urlpatterns = [
url(r'^api/', include(router.urls, namespace='api'))
]
def test_color_field_content(self):
# for list URL. e.g. /api/my-list/
path = 'api:my_list-list'
assert reverse(path) == '/api/my-list/'
found = resolve(reverse(path))
assert found2.func.__name__ == get_chats.__name__
def test_color_field_content(self):
# for detail URL. e.g. /api/my-list/<pk>/
path = 'api:my_list-detail'
assert reverse(path, args=[1]) == '/api/my-list/1/'
found = resolve(reverse(path))
assert found.func.cls == views.UserNotification
Testowanie serializatora¶
Testując widok sprawdzamy czy zwrócowna wartość wykorzystuje konkretny serializator. Nie sprawdzamy jednak samego działania serializatora, nie wiemy czy dodaliśmy do niego nowe pola, czy może nie zmieniliśmy akcji utworzenia nowego obiektu. Jeśli nasz serializator posiada co najmniej jedną rzecz, która powoduje, że mamy jakieś ograniczenia na polu lub podmieniamy domyślną metodę, wtedy musimy przetestować serializator.
Poniższy przykład pokazuje bardzo prosty serializator, jednak jak zobaczysz w testach, jest kilka rzeczy które warto sprawdzić.
from django.db import models
class Tool(models.Model):
COLOR_OPTIONS = (
('yellow', 'Yellow'),
('red', 'Red'),
('black', 'Black')
)
color = models.CharField(
max_length=255,
null=True,
blank=True,
choices=COLOR_OPTIONS)
size = models.DecimalField(
max_digits=4,
decimal_places=2,
null=True,
blank=True)
Do podanego powyżej modelu tworzymy prosty serializator.
from rest_framework import serializers
from tools.models import Tool
class ToolSerializer(serializers.ModelSerializer):
COLOR_OPTIONS = ('yellow', 'black')
color = serializers.ChoiceField(
choices=COLOR_OPTIONS)
size = serializers.FloatField(
min_value=30.0,
max_value=60.0)
class Meta:
model = Tool
fields = ['color', 'size']
Najpierw przygotowujemy naszą klasę, która będzie zawierać podstawowe dane. W każdej chwili tworzą test, będziemy mogli je podmienić.
@pytest.mark.django_db
class TestToolSerializer:
@pytest.fixture(autouse=True)
def setup_method(self, db, tool_factory):
self.tool_attributes = {
'color': 'yellow',
'size': Decimal('52.12')}
self.serializer_data = {
'color': 'black',
'size': 51.23}
self.tool = tool_factory(**self.tool_attributes)
self.serializer = ToolSerializer(instance=self.tool)
Używam zbioru pól aby upewnić się, że dane wyjściowe z serializera mają dokładnie te pola, którychy oczekujemy. Używanie zbioru do tej weryfikacji jest bardzo ważne, ponieważ gwarantuje, że dodanie lub usunięcie dowolnego pola do serializera zostanie zauważone podczas wykonywania testów.
def test_contains_expected_fields(self):
data = self.serializer.data
assert set(data.keys()) == set(['color', 'size'])
Teraz przechodzimy do sprawdzania, czy serialalizator generuje oczekiwane dane do każdego pola. Pole kolor jest dość standardowe:
def test_color_field_content(self):
data = self.serializer.data
assert data['color'] == self.tool_attributes['color']
def test_size_field_content(self):
data = self.serializer.data
assert data['size'] == float(self.tool_attributes['size'])
Atrybut size ma zarówno dolny, jak i górny limit. Bardzo ważne jest testowanie przypadków brzegowych które określiliśmy.
def test_size_lower_bound(self):
self.serializer_data['size'] = 29.9
serializer = ToolSerializer(data=self.serializer_data)
assert serializer.is_valid() is False
assert set(serializer.errors) == set(['size'])
def test_size_upper_bound(self):
self.serializer_data['size'] = 60.1
serializer = ToolSerializer(data=self.serializer_data)
assert serializer.is_valid() is False
assert set(serializer.errors) == set(['size'])
def test_float_data_correctly_saves_as_decimal(self):
self.serializer_data['size'] = 31.789
serializer = ToolSerializer(data=self.serializer_data)
serializer.is_valid()
new_tool = serializer.save()
new_tool.refresh_from_db()
assert new_tool.size == Decimal('31.79')
def test_color_must_be_in_choices(self):
self.tool_attributes['color'] = 'red'
serializer = ToolSerializer(instance=self.tool, data=self.tool_attributes)
assert serializer.is_valid() is False
assert set(serializer.errors.keys()) == set(['color'])
Do przygotowania¶
- Testowanie walidatorów oraz walidacji pól
- Testowanie własnych pól