📝

バックエンドのテスト戦略について

2022/08/24に公開約4,000字

イントロ

今現在仕事で社内用のWebアプリケーションのバックエンド部分の開発をメインで担当している。(ここでいうバックエンドとはいわゆるAPIサーバーを指す。)
インフラ部分はすでにあったものに相乗りする形にしたため何もしてない(ecsのタスク定義書き足しただけ)が、アプリケーションの部分はほとんど1から自分が書いたため、言語選定からアーキテクチャの設計といった部分も自分が考えて意思決定した。もちろん色んな人に壁打ちをして意見をもらった上でだが。
今回はそうしたバックエンドを1から開発する中で結構悩んだことの1つである「テスト戦略」について書く。

テストを書く上で大事だと思うこと

意図を明確にする

  • なぜこのコードに対してテストを書いたのか (or 書かないのか)
  • このテストは何をテストしたいのか

こうしたテストに対する意図が言語化されてると嬉しいことがいくつかある。

  • テストコードの理解がしやすい
  • 新しくテストを書くべきか否かの判断がしやすい
  • テストがコケた時にメンテしやすい

テストにかかる労力はなるべく小さく

テストの実行はなるべく簡単にできるべきであり、またテストコードのメンテナンスや開発もなるべく労力のかからない形でできるようにするべきである。なぜかというと、テストの実行やメンテにかかる労力が大きくなると、人々はテストを実行しなくなるし、テストを書かなくなる。テストは実行されて初めて意味を持つし、新たにテストを書くべき開発がなされたらテストを書かないとテストとしての価値が下がってしまう。

バックエンドのレイヤー構造

今回想定するバックエンドのアーキテクチャ(というかレイヤー)構造をここで説明する。
すごいざっくりだけど以下の図のような構造で、どこにどういうテストを書くか(書かないか)を考えていく。

図の矢印は依存関係を表している。各レイヤーの関心事は以下の通り。

  • handler: HTTP request/responseを扱う
  • usecase: 機能要件を達成するためのコアロジックを扱う
  • repository: 永続化層(今回はDB)とのやり取りをする
  • entity: サービスに登場するオブジェクトやその振る舞いを定義する

ユニットテスト

ユニットテストとはなんぞやという話はしないが、今回ユニットテストで達成したいことは、
各レイヤー単体で見た時に期待した振る舞いしてるよね?を保証することとする。

依存関係はモックする

レイヤーによっては他のレイヤーに依存している箇所が存在しているため、自分のテストをするには依存しているレイヤーのメソッドを呼び出さないといけないやん!と思うかもしれない。しかし、それはやりたくない。
なぜなら、ユニットテストはあくまでテスト対象のレイヤーのみの振る舞いをテストしたいのであって、テスト対象の依存先に興味はないからである。
なので、依存先は正しく動いてるという前提のもとでテスト対象のレイヤーのテストに集中するようにしたい。
そういった場合にはモックやフェイクオブジェクトが有効で、依存先をモックに差し替えてしまうことで依存先のことを考えなくて済む。

handlerレイヤーのユニットテストは書かない

いきなり書かないんかい!

そう、個人的にhandler層のユニットテストはいらないと思っている。
その理由は単純で、テストしたいことがない。

handler層は先程書いたとおり基本HTTP req/resのあれこれをするだけで、処理の大部分は他のレイヤーに委譲することになる。したがってhandler層はロジックがない薄い層になるはずなのでテストしたくもならない。

逆に言うと、テストしたくなるようなロジックがhandler層に紛れ込んでいる場合、handler層に他の関心事が存在している可能性があるのでなんとかしたほうがいい。

repositoryレイヤーも基本ユニットテストは書かない

また書かないんかい!

はい、repository層も基本的にはユニットテスト書かなくていいと思ってます。
なぜかというと、repository層はDBとのやりとりをする層で、repository層単体で見た時にテストしたいことがないから。DBまでテストのスコープに含めたらそれはもうインテグレーションテストになる。
複雑なSQLをクエリビルダーで組み立てるみたいなときはそういったロジックはテストしたくなるけど、シンプルなクエリだけで済んでるなら基本いらない。

DBのモックは私は好きではない

今回で言うrepositoryレイヤーのテストについてインターネットで調べると、DBをモックしてテストするぞ!みたいな記事がよく出てくる。repository単体をテストしたいからDBはフェイクオブジェクトにしてしまうという考え方はわからんでもないが、個人的にはそれで本当にテストしたいことがテストできてますか?ということを問いたい。
DBをモックしないといけないということはDBとのやりとりが必要な部分をテストしたいということである。しかし、DBをモックに差し替えた状態でテストできることは、repository - モック間のやり取りが意図通りに行われているかだけであり、本来のDBとやりとりしたときに意図通りに行われるかどうかはテストで保証されない。
repositoryのようなアプリケーションコードの外側に存在するものに依存するレイヤーは、依存先をフェイクオブジェクトに置き換えてしまうとテストがどうしても自作自演ぽくなりがちなので、私は書かないことにした。

アプリケーションコードの内側の部分は、依存先が内部でどういう振る舞いをしているかに興味がないので、モックしてもテストしたいことがテストできる。

usecaseレイヤーのユニットテスト

これは書く。で、usecaseが利用するrepository層は先程述べたとおりモックに差し替える。
このとき差し替えやすくするためにusecase層にrepositoryを抽象化したインターフェースをおいておくとかするのだが、この話は細かくしない。

なぜテストしたい?

サービス的に重要なロジックはだいたいここに書かれるので、そいつらが意図通りに動かないということはつまり機能要件を満たしてないということである。したがってこの層のメソッド群が意図通り動くことを担保することは重要である。

なにをテストしたい?

サービス的に重要なロジック 以上!

entityレイヤーのユニットテスト

entity層は別のレイヤーに依存することがないので特にむずかしいことを考えずにユニットテストがかける。

なぜテストしたい?

アプリケーションに登場する概念をここで定義して、そのオブジェクトの振る舞いもここに書かれる。
オブジェクトの振る舞いが意図通りではない場合はバグにつながるし、データの不整合も引き起こす可能性があるのでテストしたい。

なにをテストしたい?

オブジェクトの定義そのものをテストするのは無理なので、振る舞いをテストする。

インテグレーションテスト

統合テストともいう。ユニットテストが単一のレイヤー/モジュールに対するテストだったのに対して、インテグレーションテストは複数のレイヤーにまたがるテストである。

repository - DBのインテグレーションテスト

Dockerを始めとしたコンテナ環境が普及したことで、このテストはだいぶ書きやすくなったと思う。
たとえば本番のDBがMySQLだったら、手元でMySQLのDocker コンテナを立ててしまい、そいつを使ってrepository層とのやりとりをテストできる。

いついかなるときも書きたいとは思わない

まず、私が仕事で書いてるアプリケーションではrepository - DB間のインテグレーションテストは書かないという意思決定をした。その理由としては、

  1. 本番と同じようなDB環境をローカルやCI上で再現できなかった
  2. DBとのやりとりがちゃんとできるかどうかをテストしたいと思わなかった
  3. コスパが悪かった

このテストを気合を入れて書いても得られるものが少ないなと判断した。まあその一方で、DBとのやりとりがちゃんとできてないとサービスが4ぬ!!みたいなレベルだったら書くと思う。つまりケースバイケースである。
結局そのテストスコープに対してどれくらい不安を抱えているかが結構テストを書くかどうかを判断するときに大事だと思っている。

handler - usecase - repository - DB のインテグレーションテスト

つまり、一気通貫でやるテスト。

実はこのテストも書いてない。(お前全然テスト書いてないやんけ)
なんで書いてないかというと、やはり書く旨味があまりないと感じたからである。

私が開発しているサービスにはブランチ環境と呼ばれる、自分が開発しているブランチの状態を反映したアプリケーションが触れる環境が用意されていて、そこで確かめればええやんとなった。

おわりに

私が考えるテストをどこに書いてどこに書かないかという話だったが、途中にも書いたとおりサービスの特性によって判断は変わってくると思うのでそこだけは注意されたい。
指摘や意見がもしあれば気軽に書いてもらえると助かります。

Discussion

ログインするとコメントできます