Разработка на основе тестов API-интерфейса RESTful для Django
В этом посте рассматривается процесс разработки RESTful API на основе CRUD с Django иDjango REST Framework, который используется для быстрого создания RESTful API на основе моделей Django.
Это приложение использует:
-
Python v3.6.0
-
Джанго v1.11.0
-
Django REST Framework v3.6.2
-
Postgres v9.6.1
-
Psycopg2 v2.7.1
Free Bonus:Click here to download a copy of the "REST API Examples" Guide и получите практическое введение в принципы API Python + REST с практическими примерами.
NOTE: Ознакомьтесь с третьим курсомReal Python, чтобы получить более подробное руководство по Django REST Framework.
Цели
К концу этого урока вы сможете…
-
Обсудите преимущества использования 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.
-
Auth Support: REST Framework имеет широкую поддержку различных протоколов аутентификации, а также разрешений и политик регулирования, которые можно настроить для каждого просмотра.
-
Serializers: Сериализаторы - это элегантный способ проверки наборов запросов / экземпляров модели и преобразования их в собственные типы данных Python, которые можно легко преобразовать в JSON и XML.
-
Throttling: регулирование - это способ определить, авторизован ли запрос или нет, и его можно интегрировать с различными разрешениями. Обычно используется для ограничения скорости запросов 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
Установите Django и настройте новый проект:
(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
Начнем с создания приложенияpuppies
и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'
]
Затем определите глобальныйsettings для 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 для всех запросов.
NOTE: Неограниченный доступ подходит для локальной разработки, но в производственной среде вам может потребоваться ограничить доступ к определенным конечным точкам. Обязательно обновите это. Просмотритеdocs для получения дополнительной информации.
Ваша текущая структура проекта теперь должна выглядеть так:
└── 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: Не стесняйтесь заменить Postgres на реляционную базу данных по вашему выбору!
Когда в вашей системе будет работающий сервер Postgres, откройте интерактивную оболочку Postgres и создайте базу данных:
$ psql
# CREATE DATABASE puppy_store_drf;
CREATE DATABASE
# \q
Установитеpsycopg2, чтобы мы могли взаимодействовать с сервером Postgres через Python:
(env)$ pip install psycopg2==2.7.1
Обновите конфигурацию базы данных вsettings.py, добавив соответствующие имя пользователя и пароль:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'puppy_store_drf',
'USER': '',
'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.'
Теперь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)
NOTE: Вы можете запустить
\d+ puppies_puppy
, если хотите посмотреть фактические детали таблицы.
Прежде чем двигаться дальше, напишемquick unit test для модели 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.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, давайте определимserializer для нашей модели Puppy, которая проверяет модельquerysets и создает типы данных Pythonic для работы.
Добавьте следующий фрагмент в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 Структура
В RESTful API конечные точки (URL) определяют структуру API и то, как конечные пользователи получают доступ к данным из нашего приложения с помощью методов HTTP - GET, POST, PUT, DELETE. Конечные точки должны быть логически организованы вокругcollections иelements, которые являются ресурсами.
В нашем случае у нас есть один единственный ресурс,puppies
, поэтому мы будем использовать следующие URL-адреса -/puppies/
и/puppies/<id>
для коллекций и элементов соответственно:
Конечная точка | HTTP метод | CRUD метод | Результат |
---|---|---|---|
|
GET |
READ |
Получить всех щенков |
|
GET |
READ |
Завести одного щенка |
|
POST |
СОЗДАЙТЕ |
Добавить одного щенка |
|
PUT |
ОБНОВИТЬ |
Обновить одного щенка |
|
УДАЛЯТЬ |
УДАЛЯТЬ |
Удалить одного щенка |
Маршруты и тестирование (TDD)
Мы будем использовать подход, основанный на тестировании, а не тщательный подход, основанный на тестировании, при котором мы будем проходить через следующий процесс:
-
добавить модульный тест, достаточно кода, чтобы потерпеть неудачу
-
затем обновите код, чтобы он прошел тест.
Как только тест пройден, начните все заново с того же процесса для нового теста.
Начнем с создания нового файла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({})
Создайте соответствующие URL-адреса для соответствия представлениям вdjango-puppy-store/puppy_store/puppies/urls.py:
from django.conf.urls import url
from . import views
urlpatterns = [
url(
r'^api/v1/puppies/(?P[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'
)
]
Также обновитеdjango-puppy-store/puppy_store/puppy_store/urls.py:
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
Теперь, когда все маршруты подключены к функциям просмотра, давайте откроем интерфейс Browserable API REST Framework и проверим, все ли URL работают должным образом.
Сначала запустите сервер разработки:
(env)$ python manage.py runserver
Не забудьте закомментировать все атрибуты в разделеREST_FRAMEWORK
нашего файлаsettings.py
, чтобы обойти вход в систему. Теперь посетите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({})
Здесь мы получаем все записи для щенков и проверяем каждую, используяPuppySerializer
.
Запустите тесты, чтобы убедиться, что они все прошли:
Ran 2 tests in 0.072s
OK
ПОЛУЧИТЬ Одноместный
Выбор одного щенка включает два теста:
-
Получить действительного щенка - например, щенок существует
-
Получите недействительного щенка - например, щенок не существует
Добавьте тесты:
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
Вставка новой записи также включает два случая:
-
Вставка действительного щенка
-
Вставка неверного щенка
Сначала напишите для него тесты:
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({})
В приведенном выше фрагменте, аналогичном вставке, мы сериализуем и проверяем данные запроса, а затем отвечаем соответствующим образом.
Запустите тесты снова, чтобы убедиться, что все тесты пройдены.
УДАЛЯТЬ
Для удаления отдельной записи требуется идентификатор:
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 с подходом, основанным на тестировании.
Free Bonus:Click here to download a copy of the "REST in a Nutshell" Guide с практическим введением в принципы и примеры REST API.
Что дальше? Чтобы сделать наш RESTful API надежным и безопасным, мы можем реализовать разрешения и регулирование для производственной среды, чтобы разрешить ограниченный доступ на основе учетных данных аутентификации и ограничения скорости, чтобы избежать любого рода DDoS-атак. Кроме того, не забудьте запретить доступ к API Browsable в производственной среде.
Не стесняйтесь делиться своими комментариями, вопросами или советами в комментариях ниже. Полный код можно найти в репозиторииdjango-puppy-store.