Pythonのクリーンアーキテクチャ:段階的なデモ。 パート5



内容




RESTレイヤー(パート1)



Gitタグ: Step12







私たちの冒険の最終段階は、きれいな建築を求めてやって来ました。 ドメインモデル、シリアライザー、スクリプト、およびストレージを作成しました。 しかし、すべてをつなぎ合わせるインターフェースはありませんが、ユーザーから呼び出しパラメーターを受け取り、リポジトリでスクリプトを初期化し、リポジトリからドメインモデルを受け取るスクリプトを実行し、それらを標準形式に変換します。 このレイヤーは、多くのインターフェースとテクノロジーを使用して表すことができます。 たとえば、コマンドラインインターフェイス(CLI)を使用する場合:コマンドラインスイッチを使用してパラメーターを取得し、結果をコンソール上のテキストとして返します。 ただし、ウィジェットのセットから呼び出しパラメーターを受け取り、上記の手順を実行し、返されたデータをJSON形式で解析して同じページに結果を表示するWebページには、同じ基本システムを使用できます。







選択されたテクノロジーに関係なく、ユーザーと対話し、入力データを収集して出力結果を提供するには、新しく作成されたクリーンなアーキテクチャと対話する必要があります。 したがって、ここでは、HTTPを操作するためのAPIを公開するレイヤーを作成します。 これは、一部のデータが返されるアクセス時に、HTTPアドレス(APIエンドポイント)のセットを提供するサーバーを使用して実装されます。 通常、このようなレイヤーはRESTレイヤーと呼ばれます。これは、原則として、アドレスのセマンティクスがRESTの推奨事項に類似しているためです。







Flaskは、ユーザーが必要とする部分のみを提供するモジュール構造の軽量Webサーバーです。 特に、独自の実装されたストレージレイヤーがあるため、/ ORMデータベースは使用しません。



通常、この層とストレージ層は別個のパッケージとして実装されますが、このチュートリアルの一環としてそれらをまとめました。







依存関係ファイルを更新します。 prod.txt



ファイルにはFlaskモジュールが含まれている必要があります







 Flask
      
      





dev.txt



ファイルにはFlask-Script拡張が含まれています







 -r test.txt pip wheel flake8 Sphinx Flask-Script
      
      





そしてtest.txt



ファイルにtest.txt



Flaskで作業するためのpytest拡張機能を追加します(これについては後で説明します)







 -r prod.txt pytest tox coverage pytest-cov pytest-flask
      
      





これらの変更後、必ずpip install -r requirenments/dev.txt



実行して、仮想環境に新しいパッケージをインストールしてください。







Flaskアプリケーションのセットアップは簡単ですが、多くの機能が含まれています。 これはFlaskのチュートリアルではないため、これらの手順を簡単に説明します。 ただし、各機能のFlaskドキュメントへのリンクを提供します。







通常、テスト環境、開発環境、および実稼働環境に個別の構成を定義します。 Flaskアプリケーションは通常のPythonオブジェクト( ドキュメント )を使用してrentomatic/settings.py



できるため、これらのオブジェクトをホストするrentomatic/settings.py



ファイルを作成します







 import os class Config(object): """Base configuration.""" APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) class ProdConfig(Config): """Production configuration.""" ENV = 'prod' DEBUG = False class DevConfig(Config): """Development configuration.""" ENV = 'dev' DEBUG = True class TestConfig(Config): "" " ". "" ENV = 'test' TESTING = True DEBUG = True
      
      





Flask設定オプションの詳細については、 このページを参照してください 。 ここで、Flaskアプリケーションの初期化( documentation )、構成、および図面の登録( documentation )が必要です。 ファイルrentomatic/app.py



には次のコードが含まれています。







 from flask import Flask from rentomatic.rest import storageroom from rentomatic.settings import DevConfig def create_app(config_object=DevConfig): app = Flask(__name__) app.config.from_object(config_object) app.register_blueprint(storageroom.blueprint) return app
      
      





アプリケーションエンドポイントは、最新の結果とHTTPステータスを含むResponse



フラスコを返す必要があります。 この場合、応答の内容は、スクリプト応答のJSONシリアル化です。







RESTエンドポイントで何が起こるかをよく理解できるように、ステップごとにテストの記述を開始します。 テストの基本構造は次のとおりです







 [SOME PREPARATION] [CALL THE API ENDPOINT] [CHECK RESPONSE DATA] [CHECK RESPONDSE STATUS CODE] [CHECK RESPONSE MIMETYPE]
      
      





したがって、最初のテストtests/rest/test_get_storagerooms_list.py



は次の部分で構成されています







 @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase') def test_get(mock_use_case, client): mock_use_case().execute.return_value = res.ResponseSuccess (storagerooms)
      
      





ここではスクリプト自体をテストしないため、スクリプトを混在させることができます。 ドメインモデル(まだ定義していない)のリストを含むResponseSuccess



インスタンスをスクリプトに強制的に返します。









ご注意 翻訳者
残念ながら、このコードは私には機能しません(python 2.7)。 StorageRoomListUseCaseは濡れません。 モックモジュールに関する知識が乏しいため、デコレータを使用せずに直接マンキングすることでこの瞬間を決定しました。

 def test_get(client, monkeypatch): def monkey_execute(self, rqst): return res.ResponseSuccess(storagerooms) monkeypatch.setattr(StorageRoomListUseCase, 'execute', monkey_execute)
      
      





元のコードが機能しない理由を誰かが知っている場合は、お知らせください。



  http_response = client.get('/storagerooms')
      
      





これは現在のAPI呼び出しです。 エンドポイントを/storagerooms



設定しますpytest-Flaskが提供するclient



フィクスチャの使用に注意してください。







 assert json.loads(http_response.data.decode('UTF-8')) == [storageroom1_dict] assert http_response.status_code == 200 assert http_response.mimetype == 'application/json'
      
      





これらは上記の3つのチェックです。 2番目と3番目は非常に単純ですが、1番目は説明が必要です。 http_response.data



[storageroom1_dict]



を比較します。これは、 storageroom1_domain_model



オブジェクトのデータを含むPython辞書のリストです。 Response



Flaskオブジェクトにはデータのバイナリ表現が含まれているため、まずUTF-8を使用してバイトをデコードし、次にPythonオブジェクトに変換します。 Pythonオブジェクトを比較する方がはるかに便利です。なぜなら、pytestはキーの順序がないために辞書に問題がある可能性があるからです。 しかし、行を比較すると、そのような困難はありません。







ドメインモデルテストとその辞書を含む最終テストファイル:







 import json from unittest import mock from rentomatic.domain.storageroom import StorageRoom from rentomatic.shared import response_object as res storageroom1_dict = { 'code': '3251a5bd-86be-428d-8ae9-6e51a8048c33', 'size': 200, 'price': 10, 'longitude': -0.09998975, 'latitude': 51.75436293 } storageroom1_domain_model = StorageRoom.from_dict(storageroom1_dict) storagerooms = [storageroom1_domain_model] @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase') def test_get(mock_use_case, client): mock_use_case().execute.return_value = res.ResponseSuccess(storagerooms) http_response = client.get('/storagerooms') assert json.loads(http_response.data.decode('UTF-8')) == [storageroom1_dict] assert http_response.status_code == 200 assert http_response.mimetype == 'application/json'
      
      





最後に、アーキテクチャのすべての部分の動作を確認するエンドポイントを作成します。







Flaskの最小エンドポイントは、 rentomatic/rest/storageroom.py



配置できます。







 blueprint = Blueprint('storageroom', __name__) @blueprint.route('/storagerooms', methods=['GET']) def storageroom(): [LOGIC] return Response([JSON DATA], mimetype='application/json', status=[STATUS])
      
      





最初に作成するのはStorageRoomListRequestObject



です。 今のところ、オプションのクエリ文字列パラメータを無視して、空の辞書を使用できます







 def storageroom(): request_object = ro.StorageRoomListRequestObject. from_dict ({})
      
      





ご覧のとおり、空の辞書からオブジェクトを作成しているため、クエリ文字列パラメーターは考慮されません。 次に行うことは、リポジトリを初期化することです







  repo = mr.MemRepo ()
      
      





3番目は、スクリプトエンドポイントの初期化です







 use_case = uc.StorageRoomListUseCase(repo)
      
      





そして最後に、リクエストオブジェクトを渡すことでスクリプトを実行します







 response = use_case.execute(request_object)
      
      





しかし、この答えはまだHTTP応答ではありません。 明確に構築する必要があります。 HTTP応答には、 response.value



属性のJSON表現が含まれます。







 return Response(json.dumps(response.value, cls=ser.StorageRoomEncoder), mimetype='application/json', status=200)
      
      





この関数は常に正常な応答(コード200)を返すため、まだ完了していないことに注意してください。 しかし、これは筆記テストに合格するには十分です。 ファイル全体は次のようになります。







 import json from flask import Blueprint, Response from rentomatic.use_cases import request_objects as req from rentomatic.repository import memrepo as mr from rentomatic.use_cases import storageroom_use_cases as uc from rentomatic.serializers import storageroom_serializer as ser blueprint = Blueprint('storageroom', __name__) @blueprint.route('/storagerooms', methods=['GET']) def storageroom(): request_object = req.StorageRoomListRequestObject.from_dict({}) repo = mr.MemRepo() use_case = uc.StorageRoomListUseCase(repo) response = use_case.execute(request_object) return Response(json.dumps(response.value, cls=ser.StorageRoomEncoder), mimetype='application/json', status=200)
      
      





このコードは、純粋なアーキテクチャ全体の動作を示しています。 確かに、記述された関数は、文字列クエリのパラメータとエラーのあるケースを考慮していないため、まだ終了していません。







動作中のサーバー



Gitタグ: Step13







エンドポイントの欠落部分を追加する前に、動作中のサーバーを見て、実際のプロジェクトを見てみましょう。







エンドポイントにアクセスするときに結果を表示するには、リポジトリにテストデータを入力する必要があります。 明らかに、使用するストレージの不整合のため、これを行う必要があります。 実ストレージは永続的なデータソースをラップするため、このテストデータは不要になります。 ストレージを初期化するためにそれらを定義します:







 storageroom1 = { 'code': 'f853578c-fc0f-4e65-81b8-566c5dffa35a', 'size': 215, 'price': 39, 'longitude': '-0.09998975', 'latitude': '51.75436293', } storageroom2 = { 'code': 'fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a', 'size': 405, 'price': 66, 'longitude': '0.18228006', 'latitude': '51.74640997', } storageroom3 = { 'code': '913694c6-435a-4366-ba0d-da5334a611b2', 'size': 56, 'price': 60, 'longitude': '0.27891577', 'latitude': '51.45994069', }
      
      





そしてそれらを私たちの店に転送します







 repo = mr.MemRepo ([storageroom1, storageroom2, storageroom3])
      
      





これで、 manage.py



ファイルを介してFlaskを実行し、公開されたURLを確認できます。







 $ python manage.py urls Rule Endpoint ------------------------------------------------ /static/<path:filename> static /storagerooms storageroom.storageroom
      
      





そして、開発サーバーを起動します







 $ python manage.py server
      
      





ブラウザーを開いてアドレスhttp//127.0.0.1►000/storageroomsにアクセスすると、API呼び出しの結果が表示されます。 答えが読みやすいように、ブラウザーのフォーマット拡張機能をインストールすることをお勧めします。 Chromeを使用している場合は、 JSON Formatterを試してください。







RESTレイヤー(パート2)



Gitタグ: Step14







エンドポイントで実現されていない2つのケースを見てみましょう。 最初に、クエリ文字列パラメーターがエンドポイントによって正しく処理されることを確認するテストを導入します







 @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase') def test_get_failed_response(mock_use_case, client): mock_use_case().execute.return_value = res.ResponseFailure.build_system_error('test message') http_response = client.get('/storagerooms') assert json.loads(http_response.data.decode('UTF-8')) == {'type': 'SYSTEM_ERROR', 'message': 'test message'} assert http_response.status_code == 500 assert http_response.mimetype == 'application/json'
      
      





ここで、スクリプトがエラー応答を返すことを確認し、HTTP応答にエラーコードが含まれていることも確認します。 テストに合格するには、ドメイン応答コードとHTTP応答コードを関連付ける必要があります







 from rentomatic.shared import response_object as res STATUS_CODES = { res.ResponseSuccess.SUCCESS: 200, res.ResponseFailure.RESOURCE_ERROR: 404, res.ResponseFailure.PARAMETERS_ERROR: 400, res.ResponseFailure.SYSTEM_ERROR: 500 }
      
      





次に、正しいコードでFlaskレスポンスを作成する必要があります







  return Response(json.dumps(response.value, cls=ser.StorageRoomEncoder), mimetype='application/json', status=STATUS_CODES[response.type])
      
      





2番目の最終テストはもう少し複雑です。 前と同様に、スクリプトをロックしていますが、今回はStorageRoomListRequestObject



もパッチをStorageRoomListRequestObject



ます。 リクエストオブジェクトがコマンドラインからの正しいパラメーターで初期化されることを知る必要があります。 したがって、ステップごとに移動します。







 @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase') def test_request_object_initialisation_and_use_with_filters(mock_use_case, client): mock_use_case().execute.return_value = res.ResponseSuccess([])
      
      





ここで、以前と同様に、スクリプトクラスのパッチは、ユースケースがResponseSuccess



オブジェクトのインスタンスを返すことを保証します。







  internal_request_object = mock.Mock()
      
      





要求オブジェクトはStorageRoomListRequestObject.from_dict



内に作成され、ここで以前に初期化されたモックオブジェクトを関数が返すようにします。







 request_object_class = 'rentomatic.use_cases.request_objects.StorageRoomListRequestObject' with mock.patch(request_object_class) as mock_request_object: mock_request_object.from_dict.return_value = internal_request_object client.get('/storagerooms?filter_param1=value1&filter_param2=value2')
      
      





StorageRoomListRequestObject



パッチをStorageRoomListRequestObject



、既知の結果をfrom_dict()



メソッドに割り当てます。 次に、いくつかのクエリ文字列パラメーターを使用してエンドポイントに戻ります。 次のことが起こります:要求のfrom_dict()



メソッドはフィルターパラメーターで呼び出され、スクリプトインスタンスのexecute()



メソッドはinternal_request_object



で呼び出されます。







 mock_request_object.from_dict.assert_called_with( {'filters': {'param1': 'value1', 'param2': 'value2'}} ) mock_use_case().execute.assert_called_with(internal_request_object)
      
      





この新しい動作を反映し、テストを有効にするには、エンドポイント関数を変更する必要があります。 新しいFlaskメソッドstorageroom()



の最終コードstorageroom()



次のとおりです。







 import json from flask import Blueprint, request, Response from rentomatic.use_cases import request_objects as req from rentomatic.shared import response_object as res from rentomatic.repository import memrepo as mr from rentomatic.use_cases import storageroom_use_cases as uc from rentomatic.serializers import storageroom_serializer as ser blueprint = Blueprint('storageroom', __name__) STATUS_CODES = { res.ResponseSuccess.SUCCESS: 200, res.ResponseFailure.RESOURCE_ERROR: 404, res.ResponseFailure.PARAMETERS_ERROR: 400, res.ResponseFailure.SYSTEM_ERROR: 500 } storageroom1 = { 'code': 'f853578c-fc0f-4e65-81b8-566c5dffa35a', 'size': 215, 'price': 39, 'longitude': '-0.09998975', 'latitude': '51.75436293', } storageroom2 = { 'code': 'fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a', 'size': 405, 'price': 66, 'longitude': '0.18228006', 'latitude': '51.74640997', } storageroom3 = { 'code': '913694c6-435a-4366-ba0d-da5334a611b2', 'size': 56, 'price': 60, 'longitude': '0.27891577', 'latitude': '51.45994069', } @blueprint.route('/storagerooms', methods=['GET']) def storageroom(): qrystr_params = { 'filters': {}, } for arg, values in request.args.items(): if arg.startswith('filter_'): qrystr_params['filters'][arg.replace('filter_', '')] = values request_object = req.StorageRoomListRequestObject.from_dict(qrystr_params) repo = mr.MemRepo([storageroom1, storageroom2, storageroom3]) use_case = uc.StorageRoomListUseCase(repo) response = use_case.execute(request_object) return Response(json.dumps(response.value, cls=ser.StorageRoomEncoder), mimetype='application/json', status=STATUS_CODES[response.type])
      
      





グローバルFlask request



オブジェクトのクエリ文字列からパラメーターを取得していることに注意してください。 クエリ文字列パラメータがディクショナリに格納されたら、そこからクエリオブジェクトを作成するだけです。







おわりに



さて、それだけです! RESTレイヤーの一部のテストは欠落していますが、先ほど述べたように、これは完全な開発プロジェクトではなく、クリーンなアーキテクチャを実証するための実用的な実装にすぎません。 次のような変更を自分で追加してみてください。









コードを開発するときは、常にTDDアプローチに従うようにしてください。 テスト容易性は、クリーンアーキテクチャの主な機能の1つです。テストを作成することは非常に重要であり、無視しないでください。







クリーンアーキテクチャを使用するかどうかに関係なく、この記事がソフトウェアアーキテクチャの見直しに役立つことを願っています。








All Articles