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