Разработка Django RESTful API через тестирование
В этом руководстве рассматривается процесс разработки RESTful API на основе CRUD с помощью Django и Django REST Framework, который используется для быстрого создания RESTful API на основе моделей Django.
Это приложение использует:
- Python v3.6.0
- Django v1.11.0
- Django REST Framework v3.6.2
- Postgres v9.6.1
- Psycopg2 v2.7.1
Цели
После завершения этого урока вы сможете...
- Обсуждение преимуществ использования Django REST Framework для начального этапа разработки RESTful API
- Валидация наборов запросов моделей с помощью сериализаторов
- Оцените функцию Browsable API в Django REST Framework для создания более чистой и хорошо документированной версии ваших API
- Практика разработки на основе тестирования
Почему Django REST Framework?
Django REST Framework (REST Framework) предоставляет ряд мощных возможностей "из коробки", которые хорошо сочетаются с идиоматическим Django, включая:
- Browsable API: Документирует ваш API с помощью удобного для человека HTML-вывода, предоставляя красивый интерфейс, похожий на форму, для отправки данных на ресурсы и получения из них с помощью стандартных методов HTTP.
- Поддержка авторизации: REST Framework имеет богатую поддержку различных протоколов аутентификации, а также разрешений и политики дросселирования, которые могут быть настроены на основе каждого представления.
- Сериализаторы: Сериализаторы - это элегантный способ проверки кверисетов/экземпляров моделей и преобразования их в собственные типы данных Python, которые могут быть легко преобразованы в JSON и XML.
- Регулирование: Регулирование: регулирование - это способ определить, авторизован ли запрос или нет, и может быть интегрирован с различными разрешениями. Обычно он используется для ограничения скорости запросов API от одного пользователя.
Кроме того, документация легко читается и полна примеров. Если вы создаете RESTful API, где у вас есть связь один-к-одному между конечными точками API и вашими моделями, то REST Framework - это то, что вам нужно.
Установка проекта Django
Создайте и активируйте virtualenv:
$ mkdir django-puppy-store
$ cd django-puppy-store
$ python3.6 -m venv env
$ source env/bin/activate
Install Django and set up a new project:
(env)$ pip install django==1.11.0
(env)$ django-admin startproject puppy_store
Ваша текущая структура проекта должна выглядеть следующим образом:
└── puppy_store
├── manage.py
└── puppy_store
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Настройка приложений Django и REST Framework
Start by creating the puppies
app and installing REST Framework inside your virtualenv:
(env)$ cd puppy_store
(env)$ python manage.py startapp puppies
(env)$ pip install djangorestframework==3.6.2
Сейчас нам нужно настроить наш проект Django для использования REST Framework.
Сначала добавьте приложение puppies
и rest_framework
в раздел INSTALLED_APPS
в puppy_store/puppy_store/settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'puppies',
'rest_framework'
]
Затем определите глобальные настройки для REST Framework в одном словаре, опять же, в файле settings.py
:
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [],
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
Это позволяет неограниченный доступ к API и устанавливает формат теста по умолчанию JSON для всех запросов.
ПРИМЕЧАНИЕ: Неограниченный доступ подходит для локальной разработки, но в производственной среде вам может понадобиться ограничить доступ к определенным конечным точкам. Обязательно обновите это. Просмотрите документы для получения дополнительной информации.
Ваша текущая структура проекта теперь должна выглядеть следующим образом:
└── puppy_store
├── manage.py
├── puppies
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── puppy_store
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Настройка базы данных и модели
Давайте настроим базу данных Postgres и применим к ней все миграции.
NOTE: Feel free to swap out Postgres for the relational database of your choice!
Если в вашей системе есть работающий сервер Postgres, откройте интерактивную оболочку Postgres и создайте базу данных:
$ psql
# CREATE DATABASE puppy_store_drf;
CREATE DATABASE
# \q
Установите psycopg2, чтобы мы могли взаимодействовать с сервером Postgres через Python:
(env)$ pip install psycopg2==2.7.1
Update the database configuration in settings.py, adding the appropriate username and password:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'puppy_store_drf',
'USER': '<your-user>',
'PASSWORD': '<your-password>',
'HOST': '127.0.0.1',
'PORT': '5432'
}
}
Затем определите модель щенка с некоторыми основными атрибутами в django-puppy-store/puppy_store/puppies/models.py
:
from django.db import models
class Puppy(models.Model):
"""
Puppy Model
Defines the attributes of a puppy
"""
name = models.CharField(max_length=255)
age = models.IntegerField()
breed = models.CharField(max_length=255)
color = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def get_breed(self):
return self.name + ' belongs to ' + self.breed + ' breed.'
def __repr__(self):
return self.name + ' is added.'
Now apply the migration:
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Проверка
Снова зайдите в psql и убедитесь, что puppies_puppy
был создан:
$ psql
# \c puppy_store_drf
You are now connected to database "puppy_store_drf".
puppy_store_drf=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+----------------
public | auth_group | table | michael.herman
public | auth_group_permissions | table | michael.herman
public | auth_permission | table | michael.herman
public | auth_user | table | michael.herman
public | auth_user_groups | table | michael.herman
public | auth_user_user_permissions | table | michael.herman
public | django_admin_log | table | michael.herman
public | django_content_type | table | michael.herman
public | django_migrations | table | michael.herman
public | django_session | table | michael.herman
public | puppies_puppy | table | michael.herman
(11 rows)
ПРИМЕЧАНИЕ. Вы можете запустить \ d + puppies puppies, если хотите посмотреть фактические данные таблицы.
Перед тем как двигаться дальше, давайте напишем быстрый модульный тест для модели Puppy.
Добавьте следующий код в новый файл test_models.py в новую папку "tests" в папке "django-puppy-store/puppy_store/puppies":
from django.test import TestCase
from ..models import Puppy
class PuppyTest(TestCase):
""" Test module for Puppy model """
def setUp(self):
Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
Puppy.objects.create(
name='Muffin', age=1, breed='Gradane', color='Brown')
def test_puppy_breed(self):
puppy_casper = Puppy.objects.get(name='Casper')
puppy_muffin = Puppy.objects.get(name='Muffin')
self.assertEqual(
puppy_casper.get_breed(), "Casper belongs to Bull Dog breed.")
self.assertEqual(
puppy_muffin.get_breed(), "Muffin belongs to Gradane breed.")
В приведенном выше тесте мы добавили фиктивные записи в нашу таблицу puppy с помощью метода setUp() из django.test.TestCase и заявили, что метод get_breed() вернул правильную строку.
Добавьте файл __init__.py в «tests» и удалите файл tests.py из «django-puppy-store/puppy_store/puppies».
Давайте проведем наш первый тест:
(env)$ python manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.007s
OK
Destroying test database for alias 'default'...
Здорово! Наш первый модульный тест пройден!
Сериализаторы
Перед тем как перейти к созданию собственно API, давайте определим сериализатор для нашей модели Puppy, который валидирует модель querysets и создает Pythonic типы данных для работы.
Add the following snippet to django-puppy-store/puppy_store/puppies/serializers.py:
from rest_framework import serializers
from .models import Puppy
class PuppySerializer(serializers.ModelSerializer):
class Meta:
model = Puppy
fields = ('name', 'age', 'breed', 'color', 'created_at', 'updated_at')
В приведенном выше фрагменте мы определили ModelSerializer
для нашей модели щенка, проверяя все упомянутые поля. Короче говоря, если у вас есть связь один-к-одному между конечными точками API и вашими моделями - что, вероятно, необходимо, если вы создаете RESTful API - то вы можете использовать ModelSerializer для создания сериализатора.
Получив базу данных, мы можем приступить к созданию RESTful API...
RESTful Structure
В RESTful API конечные точки (URL) определяют структуру API и то, как конечные пользователи получают доступ к данным из нашего приложения с помощью методов HTTP - GET, POST, PUT, DELETE. Конечные точки должны быть логически организованы вокруг коллекций и элементов, которые являются ресурсами.
In our case, we have one single resource, puppies
, so we will use the following URLS - /puppies/
and /puppies/<id>
for collections and elements, respectively:
Endpoint | HTTP Method | CRUD Method | Result |
---|---|---|---|
puppies |
GET | READ | Get all puppies |
puppies/:id |
GET | READ | Get a single puppy |
puppies |
POST | CREATE | Add a single puppy |
puppies/:id |
PUT | UPDATE | Update a single puppy |
puppies/:id |
DELETE | DELETE | Delete a single puppy |
Маршруты и тестирование (TDD)
Мы будем использовать подход, основанный на тестировании, а не на тщательном тестировании, в рамках которого мы пройдем через следующий процесс:
- add a unit test, just enough code to fail
- then update the code to make it pass the test.
После того как тест пройден, начните с того же процесса для нового теста.
Начнем с создания нового файла django-puppy-store/puppy_store/puppies/tests/test_views.py, в котором будут храниться все тесты для наших представлений, и создадим новый тестовый клиент для нашего приложения:
import json
from rest_framework import status
from django.test import TestCase, Client
from django.urls import reverse
from ..models import Puppy
from ..serializers import PuppySerializer
# initialize the APIClient app
client = Client()
Перед тем, как начать работу со всеми маршрутами API, давайте сначала создадим скелет всех функций представления, возвращающих пустые ответы, и сопоставим их с соответствующими URL в файле django-puppy-store/puppy_store/puppies/views.py:
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Puppy
from .serializers import PuppySerializer
@api_view(['GET', 'DELETE', 'PUT'])
def get_delete_update_puppy(request, pk):
try:
puppy = Puppy.objects.get(pk=pk)
except Puppy.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
# get details of a single puppy
if request.method == 'GET':
return Response({})
# delete a single puppy
elif request.method == 'DELETE':
return Response({})
# update details of a single puppy
elif request.method == 'PUT':
return Response({})
@api_view(['GET', 'POST'])
def get_post_puppies(request):
# get all puppies
if request.method == 'GET':
return Response({})
# insert a new record for a puppy
elif request.method == 'POST':
return Response({})
Create the respective URLs to match the views in django-puppy-store/puppy_store/puppies/urls.py:
from django.conf.urls import url
from . import views
urlpatterns = [
url(
r'^api/v1/puppies/(?P<pk>[0-9]+)$',
views.get_delete_update_puppy,
name='get_delete_update_puppy'
),
url(
r'^api/v1/puppies/$',
views.get_post_puppies,
name='get_post_puppies'
)
]
Update django-puppy-store/puppy_store/puppy_store/urls.py as well:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^', include('puppies.urls')),
url(
r'^api-auth/',
include('rest_framework.urls', namespace='rest_framework')
),
url(r'^admin/', admin.site.urls),
]
Просматриваемый API
Когда все маршруты подключены к функциям представления, давайте откроем интерфейс REST Framework's Browsable API и проверим, все ли URL работают так, как ожидалось.
Сначала запустите сервер разработки:
(env)$ python manage.py runserver
Make sure to comment out all the attributes in REST_FRAMEWORK
section of our settings.py
file, to bypass login. Now visit http://localhost:8000/api/v1/puppies
Вы увидите интерактивный HTML-макет для ответа API. Аналогичным образом мы можем протестировать другие URL-адреса и убедиться, что все URL-адреса работают отлично.
Начнем с наших модульных тестов для каждого маршрута.
Маршруты
Получить все
Начните с теста для проверки найденных записей:
class GetAllPuppiesTest(TestCase):
""" Test module for GET all puppies API """
def setUp(self):
Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
Puppy.objects.create(
name='Muffin', age=1, breed='Gradane', color='Brown')
Puppy.objects.create(
name='Rambo', age=2, breed='Labrador', color='Black')
Puppy.objects.create(
name='Ricky', age=6, breed='Labrador', color='Brown')
def test_get_all_puppies(self):
# get API response
response = client.get(reverse('get_post_puppies'))
# get data from db
puppies = Puppy.objects.all()
serializer = PuppySerializer(puppies, many=True)
self.assertEqual(response.data, serializer.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Запустите тест. Вы должны увидеть следующую ошибку:
self.assertEqual(response.data, serializer.data)
AssertionError: {} != [OrderedDict([('name', 'Casper'), ('age',[687 chars])])]
Обновите представление, чтобы тест прошел.
@api_view(['GET', 'POST'])
def get_post_puppies(request):
# get all puppies
if request.method == 'GET':
puppies = Puppy.objects.all()
serializer = PuppySerializer(puppies, many=True)
return Response(serializer.data)
# insert a new record for a puppy
elif request.method == 'POST':
return Response({})
Here, we get all the records for puppies and validate each using the PuppySerializer
.
Запустите тесты, чтобы убедиться, что все они прошли:
Ran 2 tests in 0.072s
OK
Получить одного
Получение одного щенка включает в себя два тестовых случая:
- Get valid puppy - e.g., the puppy exists
- Get invalid puppy - e.g., the puppy does not exists
Добавьте тесты:
class GetSinglePuppyTest(TestCase):
""" Test module for GET single puppy API """
def setUp(self):
self.casper = Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
self.muffin = Puppy.objects.create(
name='Muffin', age=1, breed='Gradane', color='Brown')
self.rambo = Puppy.objects.create(
name='Rambo', age=2, breed='Labrador', color='Black')
self.ricky = Puppy.objects.create(
name='Ricky', age=6, breed='Labrador', color='Brown')
def test_get_valid_single_puppy(self):
response = client.get(
reverse('get_delete_update_puppy', kwargs={'pk': self.rambo.pk}))
puppy = Puppy.objects.get(pk=self.rambo.pk)
serializer = PuppySerializer(puppy)
self.assertEqual(response.data, serializer.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get_invalid_single_puppy(self):
response = client.get(
reverse('get_delete_update_puppy', kwargs={'pk': 30}))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Запустите тесты. Вы должны увидеть следующую ошибку:
self.assertEqual(response.data, serializer.data)
AssertionError: {} != {'name': 'Rambo', 'age': 2, 'breed': 'Labr[109 chars]26Z'}
Обновите представление:
@api_view(['GET', 'UPDATE', 'DELETE'])
def get_delete_update_puppy(request, pk):
try:
puppy = Puppy.objects.get(pk=pk)
except Puppy.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
# get details of a single puppy
if request.method == 'GET':
serializer = PuppySerializer(puppy)
return Response(serializer.data)
В приведенном выше фрагменте мы получаем щенка, используя идентификатор. Запустите тесты, чтобы убедиться, что все они прошли.
POST
Вставка новой записи также включает два случая:
- Inserting a valid puppy
- Inserting a invalid puppy
Сначала напишите для него тесты:
class CreateNewPuppyTest(TestCase):
""" Test module for inserting a new puppy """
def setUp(self):
self.valid_payload = {
'name': 'Muffin',
'age': 4,
'breed': 'Pamerion',
'color': 'White'
}
self.invalid_payload = {
'name': '',
'age': 4,
'breed': 'Pamerion',
'color': 'White'
}
def test_create_valid_puppy(self):
response = client.post(
reverse('get_post_puppies'),
data=json.dumps(self.valid_payload),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_create_invalid_puppy(self):
response = client.post(
reverse('get_post_puppies'),
data=json.dumps(self.invalid_payload),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Запустите тесты. Вы должны увидеть два сбоя:
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
AssertionError: 200 != 400
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
AssertionError: 200 != 201
Опять же, обновите представление, чтобы тесты прошли:
@api_view(['GET', 'POST'])
def get_post_puppies(request):
# get all puppies
if request.method == 'GET':
puppies = Puppy.objects.all()
serializer = PuppySerializer(puppies, many=True)
return Response(serializer.data)
# insert a new record for a puppy
if request.method == 'POST':
data = {
'name': request.data.get('name'),
'age': int(request.data.get('age')),
'breed': request.data.get('breed'),
'color': request.data.get('color')
}
serializer = PuppySerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Здесь мы вставили новую запись, сериализовав и проверив данные запроса перед вставкой в базу данных.
Выполните тесты снова, чтобы убедиться, что они прошли.
Вы также можете проверить это с помощью Browsable API. Снова запустите сервер разработки и перейдите по адресу http://localhost:8000/api/v1/puppies/. Затем в POST-форме отправьте следующие данные в формате application/json
:
{
"name": "Muffin",
"age": 4,
"breed": "Pamerion",
"color": "White"
}
Убедитесь, что GET ALL и Get Single также работают.
PUT
Начнем с теста на обновление записи. Как и в случае с добавлением записи, нам снова нужно проверить как корректные, так и некорректные обновления:
class UpdateSinglePuppyTest(TestCase):
""" Test module for updating an existing puppy record """
def setUp(self):
self.casper = Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
self.muffin = Puppy.objects.create(
name='Muffy', age=1, breed='Gradane', color='Brown')
self.valid_payload = {
'name': 'Muffy',
'age': 2,
'breed': 'Labrador',
'color': 'Black'
}
self.invalid_payload = {
'name': '',
'age': 4,
'breed': 'Pamerion',
'color': 'White'
}
def test_valid_update_puppy(self):
response = client.put(
reverse('get_delete_update_puppy', kwargs={'pk': self.muffin.pk}),
data=json.dumps(self.valid_payload),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_invalid_update_puppy(self):
response = client.put(
reverse('get_delete_update_puppy', kwargs={'pk': self.muffin.pk}),
data=json.dumps(self.invalid_payload),
content_type='application/json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Запустите тесты.
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
AssertionError: 405 != 400
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
AssertionError: 405 != 204
Обновите представление:
@api_view(['GET', 'DELETE', 'PUT'])
def get_delete_update_puppy(request, pk):
try:
puppy = Puppy.objects.get(pk=pk)
except Puppy.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
# get details of a single puppy
if request.method == 'GET':
serializer = PuppySerializer(puppy)
return Response(serializer.data)
# update details of a single puppy
if request.method == 'PUT':
serializer = PuppySerializer(puppy, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# delete a single puppy
elif request.method == 'DELETE':
return Response({})
В приведенном выше фрагменте, подобно вставке, мы сериализуем и проверяем данные запроса, а затем отвечаем соответствующим образом.
Выполните тесты еще раз, чтобы убедиться, что все тесты прошли.
DELETE
Для удаления одной записи требуется идентификатор:
class DeleteSinglePuppyTest(TestCase):
""" Test module for deleting an existing puppy record """
def setUp(self):
self.casper = Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
self.muffin = Puppy.objects.create(
name='Muffy', age=1, breed='Gradane', color='Brown')
def test_valid_delete_puppy(self):
response = client.delete(
reverse('get_delete_update_puppy', kwargs={'pk': self.muffin.pk}))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_invalid_delete_puppy(self):
response = client.delete(
reverse('get_delete_update_puppy', kwargs={'pk': 30}))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Запустите тесты. Вы должны увидеть:
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
AssertionError: 200 != 204
Обновите представление:
@api_view(['GET', 'DELETE', 'PUT'])
def get_delete_update_puppy(request, pk):
try:
puppy = Puppy.objects.get(pk=pk)
except Puppy.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
# get details of a single puppy
if request.method == 'GET':
serializer = PuppySerializer(puppy)
return Response(serializer.data)
# update details of a single puppy
if request.method == 'PUT':
serializer = PuppySerializer(puppy, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# delete a single puppy
if request.method == 'DELETE':
puppy.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Выполните тесты еще раз. Убедитесь, что все они прошли. Не забудьте также проверить функциональность UPDATE и DELETE в Browsable API!
Вывод и последующие шаги
В этом руководстве мы рассмотрели процесс создания RESTful API с помощью Django REST Framework с использованием подхода "тест-первый".
Бесплатный бонус: Нажмите здесь, чтобы загрузить копию руководства "Примеры REST API" и получить практическое введение в принципы Python + REST API на практических примерах.
Что дальше? Чтобы сделать наш RESTful API надежным и безопасным, мы можем реализовать разрешения и дросселирование для производственной среды, чтобы обеспечить ограниченный доступ на основе учетных данных аутентификации и ограничение скорости, чтобы избежать любых DDoS-атак. Также не забудьте запретить доступ к Browsable API в производственной среде.
Источник: https://realpython.com/test-driven-development-of-a-django-restful-api/
Вернуться на верх