テストが書きづらくなる設計の正体
— 壊れにくい設計は、テストが先に楽になる —
はじめに
ここまでの連載で、こんな話をしてきました。
- DI(依存性注入)
- Service層の役割
- EntityとDTOの分離
- トランザクションの境界
- Repositoryの責務
これらは一見、別々の設計ルールに見えます。
しかし実は、
すべて「テストの書きやすさ」に収束します。
今回は、
なぜこの設計だとテストが楽で、
逆だと地獄になるのか
を整理して、連載を締めます。
まず「テストが書きづらいコード」を思い出す
ありがちな例です。
- Controllerにロジックがある
- Entityをそのまま@RequestBodyで受ける
- Repositoryに業務判断がある
- あちこちに@Transactional
この状態でテストを書こうとすると、こうなります。
- MockMvcが必要
- DBが必要
- SpringContextが必要
- 設定が多すぎる
テストしたいのは業務ルールなのに、
周辺準備が9割。
これは偶然ではありません。
テストが重い = 責務が混ざっている
テストが書きづらいとき、
だいたい次のことが起きています。
- 業務ロジックとI/Oが混ざっている
- 境界が曖昧
- 依存関係が固定されている
つまり、
設計の歪みが、テストにそのまま現れている
状態です。
テストは設計のレントゲン写真みたいなものです。
ここまでの設計がどう効いてくるか
DI(依存性注入)
DIがあると、
UserService service = new UserService(mockRepository);
が書けます。
- 差し替え可能
- モック可能
- 単体で確認可能
「Springを起動しないテスト」が書けるのは、
DIのおかげです。
Service層の分離
Serviceにロジックが集まっていると、
service.register(name);
だけをテストできます。
- HTTPなし
- JSONなし
- Controllerなし
業務ルールだけに集中できます。
EntityとDTOの分離
DTOを使っていると、
- 入力の形
- 永続化の形
を別々に扱えます。
その結果、
- 入力バリデーションのテスト
- 業務ロジックのテスト
を分離できます。
Entityを直接触らなくて済むのは、
テストでも大きなメリットです。
トランザクションをServiceで切る
Serviceに@Transactionalがあると、
- 処理の単位
- 成功/失敗の境界
が明確になります。
テストでも、
- ここまで成功
- ここで例外
という期待値が見える。
「どこまでが一連か」が見えない設計は、
テストでも迷子になります。
Repositoryの責務を絞る
Repositoryが「取得と保存」に徹していると、
- RepositoryはDB前提のテスト
- Serviceは業務ルールのテスト
と 粒度を分けられます。
全部Repositoryにあると、
全部DBテストになります。
これは避けたい。
良い設計のサイン
設計がうまくいっていると、
こんなことが起きます。
- テストコードが自然に書ける
- モックの数が少ない
- Springを起動しないテストが多い
逆に、
「テストが面倒だから書かない」
は、設計の黄色信号です。
テストは目的ではなく、結果
誤解されがちですが、
テストを書くために設計するわけではありません
しかし、
良い設計の結果として、テストが楽になる
これはほぼ例外がありません。
- 責務が分かれている
- 境界が明確
- 依存が注入されている
この状態が、
そのままテストの書きやすさになります。
まとめ(連載の結論)
この連載で伝えたかったことを、一言で言います。
Springの設計は、全部つながっている
DI、Service、DTO、Transaction、Repository。
どれか1つだけ守っても意味は薄い。
でも全部そろうと、
- 変更に強い
- 理解しやすい
- テストしやすい
という「長く生き残るコード」になります。
おわりに
JavaやSpringは、
「とりあえず動く」までは簡単です。
でも、
動き続けるコードを書く
には、設計が必要です。
この連載が、
「なぜそう書くのか」を考える
きっかけになっていれば嬉しいです。
ここまで読んでくれて、ありがとうございました。