色々なテストコードを作成してみよう の講座。
前回はAPIの自動テスト方法を学びました。
前回のレッスンはこちら↓
前回、日記一覧取得、投稿などのAPIをテストする方法を学んだな
これでAPIのテストは全部できるかな
実は一点、大事なテストを忘れていた
今回はJWT認証付きのAPIをテストする方法をやっていくぞ
JWT認証??
やっぱり知らないか。。。
JWT認証とは、トークン認証の一種だ。簡単に図解すると。。。
APIを叩くのに、トークンが必要になる。
- 認証サーバーにユーザー名とパスワードを送ってトークンを取得する
- トークンをヘッダに付与してAPIコール
- 2でトークンが誤っていたり、トークンがなかったりするとエラー
つまりAPIコールするのにログインしてトークンをもらっておく必要がある?
そうそう
このフローをテストするにはちょっと特殊なやり方が必要になる
その辺りを解説していくぞ
テスト対象の準備
例のごとくお前のクソ日記アプリの雛形をクローンしておくぞ。
リポジトリのクローン
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary
テストコードだけ見る場合
正解を書いたブランチは下記だ。テストコードだけ見たいやつは見てくれ
# テストコードがすでに書いてあるブランチを見たい場合はこちら
$ git fetch && git checkout feature/#19_jwt
テスト対象の解説
忘れてると思うから、もう一度日記アプリの仕様を確認する。
トップページにアクセスすると、こんな画面が出てくる
仕様は下記。
- トップページにアクセスすると、日記一覧が表示される
- 各日記には下記が表示される
- タイトル
- 投稿者
- 本文
- 投稿日
- カテゴリー
今回実装する機能
今回は前回の投稿APIを作った後、JWT認証の機能を追加する。
エンドポイントごとのトークンの要否は下記だ。
method | エンドポイント | 名前 | トークンが必要か |
---|---|---|---|
GET | /api/posts/ | 投稿一覧取得 | 不要 |
POST | /api/posts/ | 新規投稿 | 要 |
GET | /api/posts/:id/ | 投稿取得 | 不要 |
PUT | /api/posts/:id/ | 投稿更新 | 要 |
PATCH | /api/posts/:id/ | 投稿一部更新 | 要 |
DELETE | /api/posts/:id/ | 投稿削除 | 要 |
つまり読み取りは可能だが、作成や更新はトークンが必要になるってこと
機能追加する
APIは前回と同様なので、下記を参照して用意してくれ。コピペでガンガン進んで構わない。
必要なパッケージをインストールする
ではJWT認証の機能を追加していくぞ
まずはパッケージインストール。
$ pip install djangorestframework-simplejwt
djangorestframework-simplejwtはJWT認証を簡単に作るパッケージだ。
Django Rest Framework(以下DRF)の関連パッケージで、DRFで作ったAPIにJWT認証の機能を付与できる。
settings.pyに設定を書く
次はbasediary/settings.py
をいじっていく。
INSTALLED_APPSにrest_framework_simplejwt
のアプリを追加する。
INSTALLED_APPS = [
...
'django_filters',
'rest_framework_simplejwt',
]
次にDRFの設定を下記のように記載する。
REST_FRAMEWORK = {
...
# 読み取りのAPIのみ認証不要
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
],
# APIの認証にJWT認証を使う
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
urls.pyにエンドポイントの設定を書く
続いて、diary/urls.py
のurlpatternsに、トークンを取得するためのエンドポイントを記載する。
...
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
...
urlpatterns = [
...
# JWTトークン生成
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
]
APIの疎通
この状態で日記一覧取得ができるか試してみよう。
まずは初期データを投入し、
$ python manage.py migrate
# 初期データ投入
$ python manage.py loaddata initial.json
$ python manage.py runserver
別タブでターミナルを開いて、日記一覧のAPIをcurlで叩いてみるぞ。
日記一覧はトークンが必要ないので、普通にGETで取得できるはずだ。
$ curl http://localhost:8000/api/posts/
[
{
"id": 1,
"user": {
"id": 1,
"username": "yhei",
"email": "yheihei0126@gmail.com"
},
"title": "日記1",
"body": "日記1本文",
"created_at": "2021-08-31T22:24:35.029000Z",
"updated_at": "2021-08-31T22:25:51.954000Z",
"categories": [
{
"name": "日記",
"slug": "diary"
}
]
},
{
"id": 2,
"user": {
"id": 1,
"username": "yhei",
"email": "yheihei0126@gmail.com"
},
"title": "日記2",
"body": "日記2本文",
"created_at": "2021-08-31T22:24:49.772000Z",
"updated_at": "2021-08-31T22:25:12.370000Z",
"categories": [
{
"name": "日記",
"slug": "diary"
}
]
},
{
"id": 4,
"user": {
"id": 1,
"username": "yhei",
"email": "yheihei0126@gmail.com"
},
"title": "日記3",
"body": "カテゴリーなしの日記",
"created_at": "2021-09-01T00:04:25.537000Z",
"updated_at": "2021-09-01T00:04:25.537000Z",
"categories": []
}
]
一方で、日記投稿はトークンがないと失敗する。
試してみよう。
$ curl -X POST \
http://localhost:8000/api/posts/ \
-H 'content-type: application/json' \
-d '{
"title": "日記タイトル",
"body": "日記本文"
}'
{"detail":"Authentication credentials were not provided."}
認証ができてないぜ!? 的なことを言われてる。たぶん。。。
そうだ。最初の説明通り、新規作成・更新系のAPIにはトークン認証が必要になる。
ではトークン取得をやってみるぞ。
/api/token/ のエンドポイントに、ユーザー名、パスワードをPOSTすると、トークンが取得できる。
$ curl -X POST \
http://localhost:8000/api/token/ \
-H 'content-type: application/json' \
-d '{
"username": "yhei",
"password": "password"
}'
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NjAyNTE5MCwiaWF0IjoxNjQ1OTM4NzkwLCJqdGkiOiIzZDcwNTMwOWY3ZDQ0YjI4OGJhYmViMWI0N2M1OTBjMCIsInVzZXJfaWQiOjF9.S_dLC79tTnk0uw0TgoPU28MCUFQwgTmQkcCIRecm_7I",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ1OTM5MDkwLCJpYXQiOjE2NDU5Mzg3OTAsImp0aSI6IjMwNGI4YmE5MGFjOTQ4Mjg4YTA3MmUxZGQwZDk4ZTY1IiwidXNlcl9pZCI6MX0.qPyO3gFO2IJJwuUbuIZybWlaEynUWVUlkkNZSwRyG9k"
}
usernameとpasswordはログインするときに使うやつだね
無事に認証できたら、リフレッシュトークン(refresh)とアクセストークン(access)が得られる。
アクセストークンをヘッダにつけて日記投稿のAPIを叩くと。。。
$ curl -X POST \
http://localhost:8000/api/posts/ \
-H 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ1OTM5MDkwLCJpYXQiOjE2NDU5Mzg3OTAsImp0aSI6IjMwNGI4YmE5MGFjOTQ4Mjg4YTA3MmUxZGQwZDk4ZTY1IiwidXNlcl9pZCI6MX0.qPyO3gFO2IJJwuUbuIZybWlaEynUWVUlkkNZSwRyG9k' \
-H 'content-type: application/json' \
-d '{
"title": "日記タイトル",
"body": "日記本文"
}'
{
"id": 5,
"user": {
"id": 1,
"username": "yhei",
"email": "yheihei0126@gmail.com"
},
"title": "日記タイトル",
"body": "日記本文",
"created_at": "2022-02-27T06:18:11.822245Z",
"updated_at": "2022-02-27T06:18:11.822318Z",
"categories": []
}
おお、日記投稿が成功した!
テストコードを書く
日記のデータをfixtureで入れる
まずはテストデータの投入だ。
diary/fixtures/test/test_api_posts.json
を作って、下記のデータを入れる。
[
{
"model": "diary.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$180000$vYCnAMtlYpZj$GlAfVlJZP6X4qTF2mcRKYHnBKOr5cFmWVdE1HVLZJOc=",
"last_login": "2021-07-13T05:09:45.196Z",
"is_superuser": true,
"username": "yhei",
"first_name": "",
"last_name": "",
"email": "yheihei0126@gmail.com",
"is_staff": true,
"is_active": true,
"date_joined": "2021-07-13T05:09:21.149Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "diary.post",
"pk": 1,
"fields": {
"user": 1,
"title": "1つめの日記",
"body": "1つめの日記本文",
"created_at": "2021-07-03T05:10:40.097Z",
"updated_at": "2021-07-22T14:22:57.007Z",
"categories": [
1
]
}
},
{
"model": "diary.category",
"pk": 1,
"fields": {
"name": "日記",
"slug": "diary",
"created_at": "2021-07-22T13:59:23.525Z",
"updated_at": "2021-07-22T13:59:23.525Z"
}
}
]
JWT認証を使ったAPIテスト
早速JWT認証のテストをやってみる。
トークン取得のテスト
まずはユーザー名とパスワードでトークン取得ができるかをテストしてみる。
from django.test import TestCase, Client
from diary.models import User, Post
from django.urls import reverse
# Create your tests here.
class TestApiPosts(TestCase):
'''投稿のAPIテスト'''
# 1. fixtureを読み込む
fixtures = ['diary/fixtures/test/test_api_posts.json']
def test_1(self):
'''トークン取得できること'''
response = Client().post(
'/api/token/',
data={
'username': 'yhei',
'password': 'password',
}
)
print(response.json())
self.assertTrue('refresh' in response.json().keys())
self.assertTrue('access' in response.json().keys())
やることは下記。
- fixtureを読み込む
- /api/token/のurlを指定
- POSTのデータにユーザー名とパスワードを入れてPOSTする
の3つだけ。
では、テストを実行してみる。
$ python manage.py test
Creating test database for alias 'default'...
{'refresh': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NjAzMDcyNCwiaWF0IjoxNjQ1OTQ0MzI0LCJqdGkiOiI2YzQ1MTFkMWQ2YjQ0OWEwOGRiZjdhYTM2ZTA0ZTk3MCIsInVzZXJfaWQiOjF9.DvJpmwj8wOJfoi0X8st1TGBKJNW16m-q0uJzs3rXjoE', 'access': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ1OTQ0NjI0LCJpYXQiOjE2NDU5NDQzMjQsImp0aSI6IjJlZTRmODA5ZTMzYzQ0NTNiNWRmYjdjYjI0MzgzNTY3IiwidXNlcl9pZCI6MX0.bxp3ej0AR-OrUsVcrSRqE3CaAWz3cDyrFLWCqFfIJ1Y'}
.
----------------------------------------------------------------------
Ran 1 test in 0.329s
OK
refreshトークンとaccessトークンが取得できてるね!
トークンを使って日記投稿ができるかテストする
では、トークンを取得して日記投稿のAPIを叩くテストを作ってみる。
下記のコードを追加する。
def test_2_2(self):
'''トークンを用いて日記を登録できること(トークン取得含む)'''
# まずログインしてトークン取得
response = Client().post(
'/api/token/',
data={
'username': 'yhei',
'password': 'password',
}
)
access_token = response.json().get('access')
response = Client().post(
'/api/posts/',
data={
'title': 'title_for_test_2',
'body': 'body_for_test_2',
},
content_type='application/json',
# 取得したトークンを設定
HTTP_AUTHORIZATION=f"Bearer {access_token}"
)
self.assertEqual(201, response.status_code)
self.assertTrue(
Post.objects.filter(
title='title_for_test_2',
body='body_for_test_2'
).exists()
)
先ほどやった通り、/api/token/ にPOSTすると下記のようなトークンが得られる。
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NjAyNTE5MCwiaWF0IjoxNjQ1OTM4NzkwLCJqdGkiOiIzZDcwNTMwOWY3ZDQ0YjI4OGJhYmViMWI0N2M1OTBjMCIsInVzZXJfaWQiOjF9.S_dLC79tTnk0uw0TgoPU28MCUFQwgTmQkcCIRecm_7I",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ1OTM5MDkwLCJpYXQiOjE2NDU5Mzg3OTAsImp0aSI6IjMwNGI4YmE5MGFjOTQ4Mjg4YTA3MmUxZGQwZDk4ZTY1IiwidXNlcl9pZCI6MX0.qPyO3gFO2IJJwuUbuIZybWlaEynUWVUlkkNZSwRyG9k"
}
↑のaccess部分を/api/posts/にPOSTする際のヘッダーに付与してやれば良い。下記の部分だな。
# 取得したトークンを設定
HTTP_AUTHORIZATION=f"Bearer {access_token}"
)
ではテストを実行してみる。
$ python manage.py test
Creating test database for alias 'default'...
System check identified some issues:
System check identified 2 issues (0 silenced).
{'refresh': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NjAzMjA4MSwiaWF0IjoxNjQ1OTQ1NjgxLCJqdGkiOiIxNGUzMTZlOGYzODE0ODczYjc4NzRmMjhlMGU1ZGI5MSIsInVzZXJfaWQiOjF9.f7k851_-Wj3fe8GeAJYORKqKil0IuuVZfjJNCz6I5F4', 'access': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ1OTQ1OTgxLCJpYXQiOjE2NDU5NDU2ODEsImp0aSI6IjQ1ZDI3NjJlZjRmZjQwZmM5OTMxZTYwN2QyOWM2MDE0IiwidXNlcl9pZCI6MX0.9iPa99LtHYTjCT2qhqDzU3p9KGLxysbHXxDX2YejYAE'}
..
----------------------------------------------------------------------
Ran 2 tests in 0.567s
OK
トークンがちゃんと使えてるね!
トークン取得を省略する方法
djangorestframework-simplejwtを使っている場合、トークンをもっと簡単に取得することができる。
simplejwtのモジュールからRefreshTokenクラスをimportして使うと、エンドポイントを叩かずにトークンを生成できる。
下記のような感じだ。
from rest_framework_simplejwt.tokens import RefreshToken
...
def test_2_1(self):
'''トークンを用いて日記を登録できること'''
# 1. RefreshTokenクラスのfor_user関数にUserを渡すとトークンが生成できる
refresh = RefreshToken.for_user(User.objects.get(pk=1))
response = Client().post(
'/api/posts/',
data={
'title': 'title_for_test_2',
'body': 'body_for_test_2',
},
content_type='application/json',
# 2. refresh.acces_tokenでUser id 1のユーザーのトークンが取れる
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}"
)
self.assertEqual(201, response.status_code)
self.assertTrue(
Post.objects.filter(
title='title_for_test_2',
body='body_for_test_2'
).exists()
)
いちいちトークン生成のエンドポイントを叩くのは手間だ。
テスト内であればRefreshTokenクラスを使っていくと良いぞ。
JWT認証付きのAPI自動テスト方法まとめ
今回は、JWT認証付きのAPIのテスト方法を学んだな
JWT認証とは、ログイン情報をもとにユーザーごとのトークンを取得。
APIコール時にトークン認証させるものだな。
トークンは下記のようにトークン取得用のAPIにログイン情報をPOSTすることで取得できる。
def test_1(self):
'''トークン取得できること'''
response = Client().post(
'/api/token/',
data={
'username': 'yhei',
'password': 'password',
}
)
print(response.json())
取得したトークンは下記のようにヘッダに付与することでAPIコール時に使用する。
response = Client().post(
'/api/posts/',
data={
'title': 'title_for_test_2',
'body': 'body_for_test_2',
},
content_type='application/json',
# 取得したトークンをヘッダに設定
HTTP_AUTHORIZATION=f"Bearer {access_token}"
)
rest_framework_simplejwtを使っていれば、トークン取得のプロセスをスキップできる。
RefreshTokenクラスを下記のように使う。
from rest_framework_simplejwt.tokens import RefreshToken
...
def test_2_1(self):
'''トークンを用いて日記を登録できること'''
# 1. RefreshTokenクラスのfor_user関数にUserを渡すとトークンが生成できる
refresh = RefreshToken.for_user(User.objects.get(pk=1))
access_token = refresh.access_token
これでテストコードの書き方はだいたい終わったのかな?
そうだな。今までの知識でほとんどのテストコードが書けるはずだ。
サーバーサイドにおいてはな
え? まだあるの?
次はフロントエンドのテスト方法を学んでいくぞ
フロントエンド?
こいつフロントエンドって何? って思ってるな
\ 講座で学んだことを即アウトプットしよう /
次の講座
Djangoの基礎を学びたい方は
Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)
『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!
動画講座で手を動かしながら、ほとんどのことが学べます。
ウサギもここから始めました。
コメント