Django RESTful APIのテスト駆動開発
この投稿では、DjangoとDjango REST Frameworkを使用してCRUDベースのRESTful APIを開発するプロセスについて説明します。これは、Djangoモデルに基づいてRESTfulAPIを迅速に構築するために使用されます。
このアプリケーションは以下を使用します:
-
Python v3.6.0
-
Django 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を実行し、実用的な例を使用してPython + RESTAPIの原則を実践的に紹介します。
NOTE: Django RESTフレームワークのより詳細なチュートリアルについては、3番目のReal Pythonコースを確認してください。
目的
このチュートリアルの終わりまでに、次のことができるようになります…
-
RESTful APIの開発をブートストラップするためにDjango REST Frameworkを使用する利点について説明します
-
シリアライザーを使用してモデルクエリセットを検証する
-
Django REST Frameworkのブラウジング可能なAPI機能を評価して、APIのよりクリーンで十分に文書化されたバージョンを作成してください
-
テスト駆動開発の実践
Django REST Frameworkを使用する理由
Django RESTフレームワーク(RESTフレームワーク)は、次のような慣用的なDjangoに適した、すぐに使用できる多くの強力な機能を提供します。
-
Browsable API:人間にわかりやすいHTML出力を使用してAPIを文書化し、標準のHTTPメソッドを使用してリソースにデータを送信してリソースからフェッチするための美しいフォームのようなインターフェイスを提供します。
-
Auth Support:RESTフレームワークは、ビューごとに構成できるアクセス許可とスロットリングポリシーに加えて、さまざまな認証プロトコルを豊富にサポートしています。
-
Serializers:シリアライザーは、モデルのクエリセット/インスタンスを検証し、それらをJSONおよびXMLに簡単にレンダリングできるネイティブPythonデータ型に変換するための洗練された方法です。
-
Throttling:スロットリングは、リクエストが承認されているかどうかを判断する方法であり、さまざまな権限と統合できます。 一般に、単一ユーザーからのAPIリクエストのレート制限に使用されます。
さらに、ドキュメントは読みやすく、例が豊富です。 APIエンドポイントとモデルの間に1対1の関係があるRESTful APIを構築している場合は、RESTフレームワークが最適です。
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フレームワークのセットアップ
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フレームワークを使用する必要があります。
まず、puppies
アプリとrest_framework
をpuppy_store/puppy_store/settings.py内のINSTALLED_APPS
セクションに追加します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'puppies',
'rest_framework'
]
次に、RESTフレームワークのグローバルsettingsを、単一のディクショナリで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をインストールして、Python経由でPostgresサーバーと対話できるようにします。
(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.'
(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を記述しましょう。
「django-puppy-store / puppy_store / puppies」内の「tests」という新しいフォルダーにあるtest_models.pyという新しいファイルに次のコードを追加します。
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.")
上記のテストでは、django.test.TestCase
からsetUp()
メソッドを介してパピーテーブルにダミーエントリを追加し、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の作成に進む前に、モデルquerysetsを検証し、使用するPythonicデータ型を生成するPuppyモデルのserializerを定義しましょう。
次のスニペットを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エンドポイントとモデルの間に1対1の関係がある場合(RESTful APIを作成する場合はおそらくそうする必要があります)、ModelSerializerを使用してシリアライザーを作成できます。
データベースを配置したら、RESTful APIの構築を開始できます…
RESTful構造
RESTful APIでは、エンドポイント(URL)はAPIの構造と、エンドユーザーがHTTPメソッド(GET、POST、PUT、DELETE)を使用してアプリケーションからデータにアクセスする方法を定義します。 エンドポイントは、両方ともリソースであるcollectionsとelementsを中心に論理的に編成する必要があります。
この場合、単一のリソースpuppies
があるため、コレクションと要素にそれぞれ/puppies/
と/puppies/<id>
のURLを使用します。
終点 | HTTPメソッド | CRUDメソッド | 結果 |
---|---|---|---|
|
GET |
READ |
すべての子犬を取得します |
|
GET |
READ |
子犬を1匹ゲット |
|
POST |
作成 |
子犬を1匹追加する |
|
PUT |
更新 |
単一の子犬を更新します |
|
DELETE |
DELETE |
子犬を1匹削除する |
ルートとテスト(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ルートを開始する前に、まず、空の応答を返すすべてのビュー関数のスケルトンを作成し、django-puppy-store/puppy_store/puppies/views.pyファイル内の適切なURLにマップします。
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({})
django-puppy-store/puppy_store/puppies/urls.pyのビューに一致するそれぞれのURLを作成します。
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
これで、すべてのルートがビュー関数と結び付けられたので、REST FrameworkのBrowsable APIインターフェースを開き、すべてのURLが期待どおりに機能しているかどうかを確認しましょう。
最初に、開発サーバーを起動します。
(env)$ python manage.py runserver
ログインをバイパスするために、settings.py
ファイルのREST_FRAMEWORK
セクションのすべての属性を必ずコメントアウトしてください。 次に、http://localhost:8000/api/v1/puppies
にアクセスします
API応答のインタラクティブなHTMLレイアウトが表示されます。 同様に、他の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
GETシングル
1つの子犬を取得するには、2つのテストケースが必要です。
-
有効な子犬を取得する-例えば、子犬が存在する
-
無効な子犬を取得する-たとえば、子犬が存在しない
テストを追加します。
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)
上記のスニペットでは、IDを使用して子犬を取得しています。 テストを実行して、すべてがパスすることを確認します。
POST
新しいレコードの挿入には、次の2つのケースも含まれます。
-
有効な子犬を挿入する
-
無効な子犬を挿入する
最初に、テストを作成します。
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)
テストを実行してください。 次の2つのエラーが表示されるはずです。
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
単一のレコードを削除するには、IDが必要です。
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)
テストを再度実行します。 それらがすべて合格していることを確認してください。 Browsable API内のUPDATEおよびDELETE機能も必ずテストしてください!
結論と次のステップ
このチュートリアルでは、Django REST Frameworkを使用してテスト優先のアプローチでRESTful APIを作成するプロセスを実行しました。
Free Bonus:Click here to download a copy of the "REST in a Nutshell" GuideとRESTAPIの原則と例の実践的な紹介。
次は何ですか? RESTful APIを堅牢で安全にするために、実稼働環境にアクセス許可と調整を実装して、認証資格情報とレート制限に基づいて制限付きアクセスを許可し、あらゆる種類のDDoS攻撃を回避できます。 また、Browsable APIが実稼働環境でアクセスできないようにすることを忘れないでください。
下のコメントにコメント、質問、ヒントを自由に投稿してください。 完全なコードはdjango-puppy-storeリポジトリにあります。