Java/DDDアプリでテストコードを書く上で気を付けていること

# はじめに

テストコードについての自分の考えを整理したいと思って言語化した第2弾。
Java / Kotlin + Spring Bootのサーバーサイドエンジニアがテストコード関連で気にする点をまとめてみた。
テストコードの必要性や共通的に気にするポイントは以下を参照。

https://qiita.com/yhornisse/items/310e3d40f192f3fe70e2

# テストコードの分類

テストコードは大きく以下の3つに分類されることが多い。

- 単体テスト
- 結合テスト
- E2Eテスト(エンドツーエンドテスト)

定義については人によって基準が異なり、曖昧だが、今回では以下とする。

|分類|説明|
|:---|:-----|
|単体テスト(UT)|クラス、またはそれと密結合ないくつかのクラスをまとめた単位|
|結合テスト(IT)|内部結合テスト。アプリ内のいくつかのユニットを結合、またはアプリやDBなどのシステム内リソースを結合した単位|
|E2Eテスト|外部結合テスト。他のシステムとの連携を含めた単位|

# 良いテストとは？

テストコードに関係なく、良いテストとして以下のような内容などが挙げられるだろう。

- 正しい検証である
  - 検証方法が正しい。本番(実際の動作)相当の検証である
  - 何回検証しても同じ結果である
- 十分な検証ができる
  - 複合的な検証ができる
  - 複雑な条件を検証できる
  - 検証結果や挙動を十分確認できる
- 実施時間が短い。即ち早く結果が分かる
  - 実行時間が短い
  - テストが独立していてそれぞれを並列に実行できる
- 検証結果がNGの場合に素早く修正できる

これらに加えて、テストコードの場合はメンテナンスコストが低いことが挙げられる。
(人間が行うテストの場合は準備が簡単って要素もありそうだが、今回は省略する)

# 種類別の特性

私の経験則的に、良いテストとして考えられる要素を重要な順番に並べ替え、種類ごとに評価すると以下のような結果となる。

|観点|UT|IT|E2E|備考|
|:---|:-----:|:-----:|:-----:|:-----|
|本番(実際の動作)相当の検証である|△|◯|◎||
|複合的な検証ができる|△|◯|◎|UTは独立している部分でないと検証が難しく、ITはIF関連の検証ができない。また、ストレステストなどもUTやITではできない|
|複雑な条件の検証ができる|◎|◯|△|部分的に入力を制御でき、mockなどが利用できる方が複雑な条件は検証しやすい|
|メンテナンスコストが低い|△|◎|△|UTは内部の依存関係による影響などで改修しやすく、E2Eは依存が多すぎて変更する機会が多い|
|検証結果や挙動を十分確認できる|◎|◯|△|mockを使えたり部分的に値の検証をできる方が細かな検証ができる。やりすぎるとメンテナンス性が低下する|
|何回検証しても同じ結果である|◎|◯|◯|独立性が低いものは低い|
|テストが独立していてそれぞれを並列に実行できる|◎|△|×|外部結合まで含めてE2E環境を作るのはコストが大きすぎるため、現実性はない|
|検証結果がNGの場合に素早く修正できる|◎|◯|△|基本的に１回のテストで検証する範囲が狭い方が原因を特定しやすい|
|実行時間が短い|◎|△|◯|実現方法にもよるが、E2Eと異なり、テスト環境を独立させるため、ITはテスト環境の初期化を行うことが多い想定。E2Eは環境の独立を諦めていることが多いので、その分ITよりは早いものとしてここでは評価する|

上に行くほど重要度が高いことを示しており、「◎ > ◯ > △ > ×」順に得不得意を表している。

UTとE2Eは得意不得意が明確に分かれ、ITはバランスが良い。

私の考えとしては、E2Eでなければ確認できない、かつE2Eで確認したい内容はそれほど多くないと思うので、UTとITをメインで考えるがの良いと考える。
というのも

- フロントや他システムとは疎結合に作られており、テストコードは結合度が高い部分ごとに検証した方がテスト観点がブレない
- IFが変更されるような場合は基本的にリグレッションテストではなく、仕様変更に伴うため、テスターがテストすることが多い
  - 内部がリファクタされることはありえるが、それはITやUTでも十分テスト可能だろう

と考えるためである。
それゆえ、E2Eは、UTとITでは検証できないが、E2Eが有効であるパターンに限って行うことで、メンテナスコストや独立性などのデメリットを最小にしたまま、E2Eの恩恵を受けることができる。
(例えばストレステストや単純な死活監視など)

また、個人的にはできるだけUTでテストできるようにメインコードを書き、ITは依存解決や結合処理などを中心にテストするのが好みである。
ただ、Kent C. Doddsの支持派のようにITを中心に考える人の方が多いかもしれない。
前述の通り、ITは他と比べて平均的にテストにおける評価要素が高く、かつUTのようにメインコードをプログラミングする際にテストコードへ依存しづらいため導入しやすいなどの理由が挙げられる(※)
結合が少ないフロントエンドのコードなどは特にその傾向があるかもしれない。
ただ、DDDでレイヤリングされたサーバーエンドのコードをメインで書く私の感覚としては、ビジネスロジックやビジネスルールなどの重要な処理に対して、複雑で大量のパターンのテストを書きやすく、実行時間が短い方がありがたい。
また、テストし易い構造の方が、メインコードが綺麗に構造化されると考えているので、メインコードを書く際にテストコードのことを考える価値は十分あると考えている。
このあたりの考え方は普段書いているコードや個人の考え方に大きく左右されるだろう。

※ 昔ながらの考え方では、UTはホワイトボックステスト、ITはブラックボックステストとして扱われるので、UTでブラックボックステストをやり易くするためにメインコードの構造を考えるのは一定仕方ないとは思っている

# DDDとテスト

DDDについてはここでは詳しく言及しないが、DDDではビジネスルールやビジネスロジックなどのドメインと呼ばれる部分を中心にソフトウェア設計を行うため、ドメイン層とそれ以外の層を分ける。
そのための具体的な実現方法として、レイヤーアキテクチャを採用している。
DDDの代表的なレイヤリングは以下の通りである。

|レイヤ|ざっくりとした説明|
|:---|:---|
|プレゼンテーション|アプリが提供するIFを定義する|
|アプリケーション|アプリが取り扱うユースケースを実現する。IFやドメインなどの他の層を繋ぐ役割。ドメイン間を統合するロジックもここで扱う|
|ドメイン|ドメインのデータモデルやビジネスロジック・ビジネスルールなどを扱う|
|インフラストラクチャ|アプリ外とのアクセスを制御する|

DDDの構造だとプレゼンテーション層とインフラストラクチャ層は外部、もしくはシステム内のアプリ外へ依存する。
そのため、その2つのUTを書いても効果は薄い。
(もちろんロジックやルールを全く持たない訳ではないので、必ずしも書かない訳ではない)
尚、インフラストラクチャ層でDBなどのシステム内リソースにアクセスする処理については、IT向きである。

アプリケーション層の依存解決についてはアプリ内の結合処理のため、UTの効果は高いとは言えない。ただ、ドメイン間を結びつけてユースケースを実現するレイヤのため、そのロジックをUTで確認することは効果があるだろう。
そのためには、依存関係の解決とロジックをできるだけ分けて書くのが良い。
例えばSpringやJakarta EEを使う場合は必要なインスタンスの生成や参照などCDIに任せるが、ロジックがフレイムワークに依存すると処理単体での検証ができなくなる。
逆に言えば、依存関係を解決する部分も含めて検証するには、ITが効果的である。

以上を踏まえて、私は以下のように考えている。

|レイヤ|ざっくりとした説明|
|:---|:---|
|プレゼンテーション|複雑なバリデーションがある時だけテストを書く|
|アプリケーション|ロジックはUTをメインで考えつつ、基本はITを書く|
|ドメイン|UTが大事。UTが書けない構造なら構造を考え直す|
|インフラストラクチャ|インフラストラクチャ層としては極力書かず、アプリケーション層を通して確認する。ただし、重要な部分やロジックを持つ場合はそこだけテストを書く|

尚、ITをプレゼンテーション層ではなく、アプリケーション層を中心で考えているのは、

- プレゼンテーション層の確認は実際の外部アクセスを通さなければ十分な検証ができない
- 異なるIFで同じユースケースの処理が実行される場合、冗長である
- 外部IFが変更されるだけでテストの改修が行われるのはメンテナンスコストが高い

などが理由である。
ただし、アプリがサービスを提供するユーザー(人間とは限らない)次第では、ライブラリを使うためにプレゼンテーション層とロジックが上手く切り離せない場合もある。
その場合はプレゼンテーション層を基点にITを考える方が都合が良い場合もあるだろう。

# Javaとテスト

前述の内容を踏まえて、普段メインコードを書く上で、もしくはテストコードを追加する際に気をつけている点は以下のような観点である。

- ブラックボックステストが書きやすい構造にする
  - ロジックやビジネスルールなどの重要部はSpringなどのフレイムワークなどに依存解決を任せず、手動で管理する
    - staticメソッドにする
    - モデルの初期化メソッドやインスタンスメソッドなどとして定義する
  - 中身を把握していなければ書けないmockは使わなくても良い構造にする
    - mockを使わなければいけない部分とそうでない部分の処理を分ける
  - 環境依存のコードはできるだけ避ける
    - 避けられない場合は環境差分のUTを書きやすい構造にできると扱いやすい
  - 実行されるたびに結果が変わるものは避ける
    - 現在日時
    - 乱数
- 無理にUTでテストしない
- 不要な検証はしない
  - privateメソッドは基本的に検証しない
  - 依存先の呼び出しは検証しない
    - 内部で呼んでいるメソッドの回数などは基本的にカウントしない
      - それを行うとホワイトボックステストになるため、メンテナス性が下がる
  - 依存元に提供する責務の範囲だけ結果を検証する
    - 例えば例外の種類だけを検証し、中に含めたエラー文言は検証しない
- メンテナンスし易いテストコードを書く
  - 網羅し易く、効率的にパターンを追加できるような記載にする
    - MethodSource / EnumSource などのパターンを網羅し易い機能を利用する
  - テスト同士が独立している
  - テストコードがメインコードに依存しない
    - テスト結果を検証する場合、メインクラスの定数を参照したりするとメンテナンスコストを下げられることがあるが、リグレッションテストでの誤検知が発生したりする