サーバサイドアプリケーションの実装とテスト
今回作成するのは、非常に単純なアプリケーションです。/api/pingのURLにHTTPのGETリクエストを行った際に、pongという値が含まれたJSONメッセージを含んだHTTPレスポンスを返す非常に単純なアプリケーションです(図7)。これを実装し動作確認していきましょう。今後は今回作成するAPIのことをPingPong APIと呼びます。
図7:PingPong APIの動作イメージ
DjangoおよびDjango REST Frameworkの導入については、ここでは省略します。それぞれの導入方法の詳細については、Djangoの公式ドキュメントのクイックインストールガイドや、Django REST frameworkの公式ドキュメントのInsallationを参照してください。さらにPythonのバージョンの切り替えにpyenv、各種パッケージの導入環境の分離にvenv)を用いています。導入にはそれぞれの公式ドキュメントを参照してください。
サーバサイドアプリケーションの下準備
サーバサイドアプリケーションはDjangoを利用します。pipコマンドを用いて容易に導入が可能です(リスト2)。
Djangoのインストールが完了したら、Djangoのプロジェクトを作成します(リスト3)。
リスト3:リスト3:Djangoのsampleappプロジェクトの作成
3 | $ django-admin startproject sampleapp |
コマンドを実行したディレクトリにsampleappディレクトリが作成されているはずです。では、一度立ち上げて正常にDjangoのプロジェクトを起動できるかを確認しましょう(リスト4)。
リスト4:リスト4:動作確認用にsampleappプロジェクトを起動させる
02 | $ python manage.py runserver |
03 | Performing system checks... |
05 | System check identified no issues (0 silenced). |
07 | You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. |
08 | Run 'python manage.py migrate' to apply them. |
10 | February 03, 2019 - 10:37:26 |
11 | Django version 2.1.5, using settings 'sampleapp.settings' |
13 | Quit the server with CONTROL-C. |
それでは、http://127.0.0.1:8000にアクセスしてみましょう。下の図8に示すような画面が表示されれば、Djangoのプロジェクトは正常に構成されています。
図8:Djangoの初期画面
Djangoのsampleappプロジェクトに、今回実装するアプリケーションのディレクトリを追加していきます。
リスト5:リスト5:RESTアプリケーションをDjangoのsampleappプロジェクトへ追加する
2 | $ python manage.py startapp rest |
Django REST frameworkと、ここで作成したRESTアプリケーションを、Django内部で利用できるようにserver/sampleapp/sampleapp/settings.pyを修正しましょう。INSTALLED_APPSの配列にrest_frameworkとrestを追加します(リスト6)。また、合わせて外部からのアクセスを受け付けられるように、ALLOWED_HOSTS = ["*"]としておきます。
リスト6:リスト6:INSTALLED_APPSの修正(server/sampleapp/sampleapp/settings.pyから抜粋)
04 | 'django.contrib.admin', |
05 | 'django.contrib.auth', |
06 | 'django.contrib.contenttypes', |
07 | 'django.contrib.sessions', |
08 | 'django.contrib.messages', |
09 | 'django.contrib.staticfiles', |
10 | 'rest_framework', # Django REST framework |
11 | 'rest', # restアプリケーションの追加 |
今回のアプリケーションでは、日本語で利用することを想定しているため、言語設定とタイムゾーンを日本に指定しましょう。LANGUAGE_CODEをja、TIME_ZONEをAsia/Tokyoとします(リスト7)。
リスト7:リスト7:言語設定の変更(server/sampleapp/sampleapp/settings.pyから抜粋)
3 | TIME_ZONE = 'Asia/Tokyo' |
先ほど、python manage.py startppでRESTアプリケーションを追加しました。リクエストがRESTアプリケーションにもルーティングできるように、server/sampleapp/sampleapp/urls.pyを修正します(リスト8)。
リスト8:リスト8:RESTアプリケーションへのルーティングの追加(server/sampleapp/sampleapp/urls.py)
1 | from django.contrib import admin |
2 | from django.urls import path |
3 | from django.urls import include |
6 | path('admin/', admin.site.urls), |
7 | path('api/', include('rest.urls')), # RESTアプリケーションを参照するよう指定 |
これで、http://127.0.0.1:8000/apiのパス以下にアクセスする際は、RESTアプリケーションの配下のurls.pyにてリクエストのルーティング処理が行われるようになります。では、ルーティングが適切に行われるように、server/sampleapp/rest/urls.pyを作成しましょう(リスト9)。
リスト9:リスト9:server/sampleapp/rest/urls.py
1 | from django.urls import path |
現時点ではルーティングも何も記述されていませんが、これでアプリケーション自体の骨格は完成しました。最初のコミットはこれからのアプリケーションの基礎となるのでリモートリポジトリのmasterブランチにコードをプッシュしましょう。これでようやくサーバアプリケーションの骨格が整いました。これ以降は実際のアプリケーションの作成を進めていきましょう。
PingPong APIの仕様整理とテストコード記述
実装を始める前に改めてPingPong APIの仕様を整理し、テストを記述していきましょう。テストを事前に記述することで以下のような効果が得られます。
- 実装のゴールの明確化
- 追加実装やリファクタリングに伴い破壊的な変更が存在しないかのチェック
今回は新規の実装なので、得られる効果は主に前者の「実装のゴールの明確化」になります。
仕様を明確にしてテスト記述に備える
さて、今回作成するPingPong APIの仕様ですが、図7のように、単純に図示できる程度のシンプルなものです。本格的に複雑なものを作成する際には、リクエストとレスポンスの関係をより詳細かつ正確に記述するために、OpenAPI Specificationの利用を検討しても良いでしょう※1。
※1:Open API specification 3.0を利用する際にはSwaggerなどをコミュニケーションに活用すると良いかもしれません。
ただし、今回の事例では作成するものが非常にシンプルなので、特にそういったものは利用せずに実装を進めます。
図7:PingPong APIの動作イメージ(再掲)
リクエストの仕様を表2に示します。/api/pingにGETリクエストを投げるだけのシンプルな仕様です。
表2:PingPong APIリクエストの仕様
URL | メソッド | ヘッダ | ボディ |
/api/ping | GET | 特になし | GETなのでなし |
次にレスポンスメッセージの仕様です。ヘッダについては特に何かしらのものを具体的に指定することはなく、レスポンスボディに以下のリスト10に示すようなフォーマットのJSONを含めるだけです。
リスト10:リスト10:PingPong APIレスポンスボディの仕様
以降ではテストコードの記述、実装コード本体の記述を進めていきます。作業用に別ブランチ(feature/ping-pong-api)を作成して、そのブランチで作業を進めていきましょう(リスト11)。
リスト11:リスト11:実装用のフィーチャブランチの準備
1 | $ git checkout -b feature/ping-pong-api |
PingPong APIのテストコードを記述する
リクエスト、レスポンスの仕様を記述することで、入出力の仕様が明確化されました。では、ロジックの実装の前にテストコードを記述していきましょう。Django REST frameworkのテストの書き方の詳細については、公式ドキュメントを参照してください。今回記述しようとしているPingPong APIのテストコードは、リスト12に示すような形になります。
リスト12:リスト12:server/sampleapp/rest/tests/test_ping.py※2
01 | from django.urls import reverse |
02 | from django.test import TestCase |
03 | from rest_framework import status |
04 | from rest_framework.test import APIClient |
06 | class TestPing(TestCase): |
07 | def test_ping_view(self): |
09 | ping-pong APIにリクエストを投げた際に、 |
10 | HTTPステータスコードとして200 OK、 |
11 | レスポンスボディとして以下のようなJSONを期待する。 |
18 | path = reverse('pingpong') # pingpongという名前のついたURLを逆引きする |
19 | response = client.get(path) |
21 | self.assertEqual(response.status_code, status.HTTP_200_OK) |
22 | self.assertEqual(response.data.get('message'), "pong") |
※2:パッケージとして認識させるために、当該コードが配置されているディレクトリには__init__.pyという名前をもつファイルの配置を忘れないようにしてください。
もちろんですが、このままだと実装が存在しないので、以下に示すとおりテストは失敗します(リスト13)。
リスト13:リスト13: 実装が存在しないために失敗しているテスト
01 | $ python manage.py test |
02 | Creating test database for alias 'default'... |
03 | System check identified no issues (0 silenced). |
05 | ====================================================================== |
06 | ERROR: test_ping_view (rest.tests.test_ping.TestPing) |
07 | ---------------------------------------------------------------------- |
08 | Traceback (most recent call last): |
09 | File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/server/sampleapp/rest/tests/test_ping.py", line 18, in test_ping_view |
10 | path = reverse('pingpong') |
11 | File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/sample/lib/python3.6/site-packages/django/urls/base.py", line 90, in reverse |
12 | return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs)) |
13 | File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/sample/lib/python3.6/site-packages/django/urls/resolvers.py", line 622, in _reverse_with_prefix |
14 | raise NoReverseMatch(msg) |
15 | django.urls.exceptions.NoReverseMatch: Reverse for 'pingpong' not found. 'pingpong' is not a valid view function or pattern name. |
17 | ---------------------------------------------------------------------- |
21 | Destroying test database for alias 'default'... |
これ以降は、このテストを通過させることをゴールとして実装を追加していきましょう。
PingPong APIを実装する
Django REST frameworkを使ったAPI実装の流れは、以下のような流れになります。
- シリアライザ(Serializer)を作成する
- ビュー(View)を作成する
- 作成したビューへのルーティングをurls.pyに記述する
まず、Django REST frameworkにおけるシリアライザですが、複雑なデータをPythonのネイティブオブジェクト(辞書や配列など)に変換したり、さらにそのネイティブオブジェクトをJSONやXMLに変換、またはその逆の処理を助けてくれたりするものだと考えてください。様々な種類のシリアライザが存在するので、詳細については[公式ドキュメントのSerializersのAPIガイド](https://www.django-rest-framework.org/api-guide/serializers/)を参照してください。
今回の場合は、以下のようなメッセージ(リスト10)をPythonのオブジェクトの形式に変換する必要があります。
リスト14:リスト10:PingPong APIレスポンスボディの仕様(再掲)
では、シリアライザの実装をしてしまいましょう。server/sampleapp/rest/serializers/ping_serializer.pyを下記のとおり作成しました(リスト14)。
リスト15:リスト14:server/sampleapp/rest/serializers/ping_serializer.py
1 | from rest_framework import serializers |
3 | class PingSerializer(serializers.Serializer): |
4 | message = serializers.CharField(default='pong') |
ここで実装したPingSerializerを使って、PythonオブジェクトをJSONの形式に変換します。シリアライザの実装が終わったので、次はビューの作成に移ります。server/sampleapp/rest/views/ping_view.pyを作成します(リスト15)。
リスト16:リスト15:server/sampleapp/rest/views/ping_view.py
01 | from rest_framework.views import APIView |
02 | from rest_framework.response import Response |
04 | from rest.serializers.ping_serializer import PingSerializer |
06 | class PingView(APIView): |
08 | GETリクエストに対してpongの文言をボディに含んだ |
09 | HTTPレスポンスメッセージを返すAPIです。 |
12 | def get(self, request, format=None): |
16 | serializer = PingSerializer(message) |
17 | return Response(serializer.data) |
PingViewではGETリクエストメッセージを受け取ると、messageキーとその値を含んだ辞書を作成し、PingSerializerでJSON形式に変換してレスポンスを返しています。
最後に/api/pingのURLパスでアクセスがあった場合にリクエストをPingViewに処理させるために、server/sampleapp/rest/urls.pyを設定しましょう(リスト16)。
リスト17:リスト16:server/sampleapp/rest/urls.py
1 | from django.urls import path |
2 | from django.contrib import admin |
4 | from rest.views.ping_view import PingView |
7 | #/api/pingに来たリクエストをPingViewに流す |
8 | path('ping', PingView.as_view(), name="pingpong"), |
さて、リクエストのルーティングまで実装が完了したので、再度テストを実行しましょう(リスト17)。
リスト18:リスト17:テストの実行結果
2 | Creating test database for alias 'default'... |
3 | System check identified no issues (0 silenced). |
5 | ---------------------------------------------------------------------- |
9 | Destroying test database for alias 'default'... |
ようやくテストをパスすることができました。仕様として予め定めたとおりに実装することができたようです。ここで補足ですが、Django REST frameworkでは、APIの動作確認をするためのUIが存在しています。ここまでで実装したPingPong APIも、ブラウザから動作確認することができます。python manage.py runserverでDjangoの開発用サーバを立ち上げてから、Webブラウザでhttp://127.0.0.1/api/pingにアクセスしてみましょう。正常に動作していれば、図9のような画面が表示されます。
図9:Django REST frameworkのWeb UI画面
このようにブラウザからAPIの動作確認を行うことも可能です。しかし、これは実装したAPIが期待した動作をしていない場合の原因特定時など、補助的な利用に留めるようにしましょう。
テストはきちんとコード化して管理することが原則です。