JWT認証付きのAPIを自動テストする

APIの自動テスト色々なテストコードを作成してみよう

↓↓↓はじめて講座を受ける方はこちら↓↓↓
開発環境を作ろう

色々なテストコードを作成してみよう の講座。

前回はAPIの自動テスト方法を学びました。

前回のレッスンはこちら↓

ウサギ
ウサギ

前回、日記一覧取得、投稿などのAPIをテストする方法を学んだな

わいへい
わいへい

これでAPIのテストは全部できるかな

ウサギ
ウサギ

実は一点、大事なテストを忘れていた

ウサギ
ウサギ

今回はJWT認証付きのAPIをテストする方法をやっていくぞ

わいへい
わいへい

JWT認証??

ウサギ
ウサギ

やっぱり知らないか。。。

ウサギ
ウサギ

JWT認証とは、トークン認証の一種だ。簡単に図解すると。。。

APIを叩くのに、トークンが必要になる。

  1. 認証サーバーにユーザー名とパスワードを送ってトークンを取得する
  2. トークンをヘッダに付与してAPIコール
  3. 2でトークンが誤っていたり、トークンがなかったりするとエラー
わいへい
わいへい

つまりAPIコールするのにログインしてトークンをもらっておく必要がある?

ウサギ
ウサギ

そうそう

ウサギ
ウサギ

このフローをテストするにはちょっと特殊なやり方が必要になる

ウサギ
ウサギ

その辺りを解説していくぞ

テスト対象の準備

例のごとくお前のクソ日記アプリの雛形をクローンしておくぞ。

リポジトリのクローン

GitHub - yheihei/base_diary: ウサでも分かる中級Django講座の教材
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary

テストコードだけ見る場合

ウサギ
ウサギ

正解を書いたブランチは下記だ。テストコードだけ見たいやつは見てくれ

GitHub - yheihei/base_diary at feature/#19_jwt
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
# テストコードがすでに書いてあるブランチを見たい場合はこちら
$ 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仕様
ウサギ
ウサギ

つまり読み取りは可能だが、作成や更新はトークンが必要になるってこと

機能追加する

APIは前回と同様なので、下記を参照して用意してくれ。コピペでガンガン進んで構わない。

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"
      }
  }
]

ユーザ名とパスワードは下記

username: yhei
password: password

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())

やることは下記。

  1. fixtureを読み込む
  2. /api/token/のurlを指定
  3. 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フレームワークを徹底解説!

動画講座で手を動かしながら、ほとんどのことが学べます。

ウサギもここから始めました。

コメント

タイトルとURLをコピーしました