HTTPハンドラーの再検討
Revisiting HTTP Handlers
このサイトにはすでにHTTPハンドラーのテスト に関する章がありますが、これはそれらの設計に関する幅広い議論を特徴とするため、テストは簡単です。
実際の例を見て、単一責任の原則や懸念の分離などの原則を適用することによって、それがどのように設計されるかを改善する方法を見ていきます。これらの原則は、インターフェース(interfaces) and 依存性注入(dependency injection)を使用して実現できます。これを行うことで、ハンドラーのテストが実際に非常に簡単であることを示します。

HTTPハンドラーのテストはGoコミュニティで繰り返し発生する問題のようです。 私は、HTTPハンドラーの設計方法を誤解している人々のより広い問題を指摘していると思います。
そのため、テストの難しさは、実際にテストを書くことよりも、コードの設計に起因することがよくあります。この本の中で、私はしばしば強調しています。
テストがあなたを苦しめているなら、そのシグナルに耳を傾け、コードの設計について考えてみてください。
例
mongodbに依存するHTTPハンドラをテストするにはどうすればよいですか?
これがコードです
この1つの関数が実行しなければならないすべてのことをリストしましょう。
HTTP応答を記述し、ヘッダー、ステータスコードなどを送信します。
リクエストの本文を
Userにデコードします。データベースに接続します(およびその周辺のすべての詳細)
データベースにクエリを実行し、結果に応じていくつかのビジネスロジックを適用する
パスワードを生成する
レコードを挿入する
これはやりすぎです。
HTTPハンドラーとは何ですか?
特定のGoの詳細を一瞬忘れてしまいますが、私が常にうまく機能してきた言語で働いていても、懸念の分離と単一責任原則.
これは、あなたが解決しようとしている問題によっては、適用するにはかなり厄介なことになります。責任とは何か?
どれだけ抽象的に考えているかによって線がぼやけてしまうこともありますし、最初の推測が正しいとは限らないこともあります。
ありがたいことに、HTTPハンドラを使うと、どのようなプロジェクトであっても、何をすべきかは大体わかっているような気がします。
HTTPリクエストを受け入れ、それを解析して検証する。
ステップ1で得たデータを使って
ServiceThingを呼び出してImportantBusinessLogicを実行する。ServiceThingが返す内容に応じて、適切なHTTPレスポンスを送信する。
すべてのHTTPハンドラがこのような形をしているべきだと言っているわけではありませんが、私の場合は 100回中99回はこのような形をしているようです。
これらの問題を切り離すと、以下のようになります。
ハンドラのテストは簡単になり、少数の懸念事項に集中することができます。
重要なことは、
ImportantBusinessLogicをテストする際に、HTTPを気にする必要がなくなり、ビジネスロジックをきれいにテストできるようになることです。ビジネスロジックをきれいにテストすることができます。
ImportantBusinessLogicが動作を変更しても、インターフェイスが同じである限り、ハンドラーを変更する必要はありません。
Go'sのハンドラ
HandlerFunc型は、通常の関数をHTTPハンドラとして利用できるようにするためのアダプタです。
type HandlerFunc func(ResponseWriter, *Request)
読者の皆さん、一呼吸おいて上のコードを見てください。何に気がつきましたか?
いくつかの引数を取る関数です
フレームワークの魔法もアノテーションも魔法の豆も何もありません。
ただの関数であり、関数のテスト方法は知っています。
これは上の解説とうまく一致しています。
http.Requestを取得して、それを検査、解析、検証するためのデータの束にしています。
超基本的な例題テスト
関数をテストするために、関数を呼び出します。
テストのためにhttptest.ResponseRecorderをhttp.ResponseWriterの引数に渡し、この関数はこれを使ってHTTPレスポンスを書きます。レコーダーは何が送られてきたかを記録します。
ハンドラでのServiceThingの呼び出し
ServiceThingの呼び出しTDDチュートリアルについてよくある苦情は、いつも「シンプルすぎて」「現実世界では十分ではない」というものです。それに対する私の答えは次のとおりです。
あなたが言及している例のように、すべてのコードが読みやすく、テストしやすいものであればいいのではないでしょうか?
これは、私たちが直面している最大の課題の一つですが、努力し続ける必要があります。コードを設計することは可能だし、良いソフトウェア工学の原則を実践して適用すれば、読みやすく、テストしやすいものになるでしょう。
先ほどのハンドラの動作を復習します。
HTTPレスポンスを書いて、ヘッダやステータスコードなどを送る。
リクエストの本文を
Userにデコードする。データベースに接続する(その辺の細かいことも含めて)
データベースに問い合わせ、結果に応じていくつかのビジネスロジックを適用する
パスワードを生成する
レコードを挿入する
より理想的な懸念の分離のアイデアを取って、私はそれがより多くのようにしたいと思います。
リクエストのボディを
Userにデコードする。UserService.Register(user)を呼び出します(これはServiceThing)エラーが発生した場合はそれに対処する(例では常に
400 BadRequestを送信しているが、これは正しいとは思えないので、500 Internal Server Errorのキャッチオールハンドラを用意しておくことにする。すべてのエラーに対して500を返すと、ひどいAPIになってしまうことを強調しておかなければなりません。後でエラー処理をもっと洗練させて、おそらくerror typesを使うことができるでしょう。エラーがなければ、
201 CreatedのIDをレスポンスボディにします(これもまた緊張感と面白さのためです)
簡潔にするために、通常のTDDプロセスについては説明しません。
新しいデザイン
このRegisterUserメソッドはhttp.HandlerFuncの形と一致しているので、これで問題ない。これを新しいタイプのUserServerのメソッドとしてアタッチしていますが、これにはUserServiceへの依存関係が含まれています。
インターフェースはHTTPの問題を特定の実装から切り離すための素晴らしい方法です。 依存関係にあるメソッドを呼び出すだけで、ユーザがどのように登録されるかを気にする必要はありません。
TDDに続いてこのアプローチをより詳しく知りたい場合は、依存性注入(Dependency Injection)の章とHTTPサーバーの章を参照してください。
これで、登録に関する特定の実装の詳細から切り離すことができたので、ハンドラのコードを書くのは簡単で、先に説明した責任に従うことになります。
さぁテストだ!
このシンプルさがテストに反映されています。
このハンドラは特定のストレージの実装とは結合されていないので、MockUserServiceを書くことで、シンプルで高速なユニットテストを書くことができます。
データベースのコードはどうでしょうか? データベースのコードはどうですか?
これはすべて意図的なものです。ビジネスロジックやデータベース、接続などに関係するHTTPハンドラは必要ありません。
このようにすることで、ハンドラを面倒な詳細から解放することができました。 また、無関係なHTTPの詳細に結合されることがなくなるので、永続化レイヤやビジネスロジックのテストがより簡単になりました。
あとは、使いたいデータベースを使ってUserServiceを実装するだけです。
これを別々にテストして、mainで満足したら、作業用アプリケーションのためにこの2つのユニットを一緒にスナップすることができます。
少ない労力で、より堅牢で拡張性の高いデザインを実現
これらの原則は、短期的に私たちの生活を楽にするだけでなく、将来的にシステムを拡張しやすくします。
このシステムをさらに改良していくと、登録確認のメールをユーザーに送るようになっても不思議ではありません。
古い設計では、ハンドラとその周辺のテストを変更しなければなりませんでした。これは、コードの一部がメンテナンス不可能になることがよくあることです。
インターフェイスを使って問題を分離することで、登録に関するビジネスロジックには関係ないので、ハンドラを編集する必要は全くありません。
まとめ
GoのHTTPハンドラのテストは難しくありませんが、優れたソフトウェアの設計は難しくなります。
人々はHTTPハンドラを特別なものだと勘違いして、それを書くときに優れたソフトウェアエンジニアリングのプラクティスを捨ててしまい、テストが難しくなってしまうのです。
もう一度言いますが、GoのHTTPハンドラは単なる関数です。 他の関数と同じように、責任を明確にして、懸念事項をしっかりと分離して書けば、テストに問題はなく、コードベースはより健全なものになります。
最終更新
役に立ちましたか?