テストを書く方針と原則の備忘録

こんにちは。サーバエンジニアのnsym-mです。普段はGoでバックエンドの開発などをしています。
最近テストに関する書籍や記事などを色々読み漁ったので、現時点での自分のテストについての考え方を備忘録として残しておきます。
今回の話はWebフロントエンドやiOS/Androidなどでも適用できる汎用的な考え方として記載していますが、ベースの文脈はバックエンド開発になりますのでそのつもりで読んでいただけますと幸いです:pray:

なお、本記事では主にGoogle、『単体テストの考え方/使い方』、[@t_wada](https://x.com/t_wada)さんの発表されている考え方（いわゆる古典学派）に倣っています。

## 用語整理

よく使われるテストスコープ
- 単体テスト（ユニットテスト）
    - 人によって定義に差がある
- 統合テスト（インテグレーションテスト）
- 結合テスト（E2Eテスト）

単体テストの定義がブレることから、スコープではなく実行時間で判断するテストサイズという考え方をGoogleが提唱しています。
- スモール
- ミディアム
- ラージ

![75c825b486ad-20240602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/494315/4af71740-d269-dc6c-d5bf-c567058d0bf1.png)
ref: https://testing.googleblog.com/2010/12/test-sizes.html

![image3-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/494315/d326c7fe-5255-61aa-d65b-a8c642db6a90.png)
ref: https://levtech.jp/media/article/column/detail_496/#ttl_4

本記事では単体テスト＝スモール、統合テスト＝ミディアムに相当するものとして記載します（E2Eについては言及しません）。
『Googleのソフトウェアエンジニアリング』によると単体テストは全体の80%、統合テスト＋E2Eテストで20%になるのが良い割合とされています。

## 単体テストで重視したいこと

- リファクタリング耐性
- スコープ対象内の信頼性
- テスト自体の保守性
- 実行容易性

#### リファクタリング耐性

- 出力値ベースもしくは状態ベース（関数実行後の状態を取得して検証）でテストする
    - 関数内の詳細の実装を検証するテストは書かない
    - ブラックボックステストでテストする
- 関数の「実装」にではなく「**振る舞い**」に対してテストする
    - その関数が「何をする機能として振る舞うか」という目的を達成できているかテストする
        - （＝出力値ベースもしくは状態ベースのテスト）
- テストはパブリック関数に書く
    - プライベート関数に書くと実装の詳細に依存した脆いテストになる
- モックは出来るだけ使用しない
    - 実装内部で呼び出す関数に依存しテストが脆くなる
        - モックにはテストを書きやすくするというメリットもあるが、脆いテストが増えてしまうのはむしろ負債となるので避ける方が良い
    - DBをモックするとクエリ側に含まれたビジネスロジックを検証できず偽陰性（テストでのバグの見落とし）が発生しうる
        - クエリからビジネスロジックを完全に取り除くのは難しい
    - モックを使用するケース
        - アプリケーションの管理下にないプロセス外依存
            - 外部サービスの提供するWeb API
            - フロントエンド/アプリから見たWeb API
        - DBはバックエンド目線でいえば管理下にあるプロセス外依存と言えるので基本モックにすべきでない
            - DBが複数アプリケーションとの共通利用などであれば管理下にない場合もある
    - 使ってもいいかもと悩むケース
        - DBを呼び出すUseCaseの役割をする関数が複雑でその関数単体でも（クエリの検証にはならないとしても）ロジックのテストを書きたい場合
            - 先にリファクタリングしてロジックをモデルに移すべき

#### スコープ対象内の信頼性

- 異常系テストをたくさん書く
    - 統合テストでは異常系を書きづらいので単体テストで出来るだけカバーする
- 偽陽性を防ぐ
    - テスト対象にバグはないのにテストが失敗してしまうこと
        - DBカラム変更で関係ない既存のテストが失敗した、など
    - 偽陽性のテスト失敗が多いとテスト結果が信頼されなくなり重要なバグを見逃す
    - リファクタリング耐性の項目で記載した内容を実施することで防げる
- テスト対象スコープが限られていることから偽陰性の発生を防ぐのは難しい
    - より上位の信頼性担保は統合テストの役割

#### テスト自体の保守性

- テストコードにロジックを含めない
- テストの保守性が悪いとどうなるか
    - 機能改修時に認知負荷が上がり修正に時間がかかるようになる
- 保守性の理想は「機能要件が変わらない限り2度と変更の必要がないテスト」
- 失敗時のエラーメッセージを明確にする
    - どこに問題があり、何を直せばいいのかメッセージだけでわかる
- テストする挙動に合わせて命名する
    - 日本人のチームであれば関数名は日本語の方が良いかも

#### 実行容易性

- ローカルでもコマンド１つで簡単に実行できる
- プッシュ時、PR作成時などにCIで自動実行される

## 統合テストで重視したいこと

- リファクタリング耐性
- 単体テストでカバーできない範囲の信頼性
- 保守性（出来るだけ）

#### リファクタリング耐性

- インターフェイスを保っていれば内部は自由にリファクタリングが可能になる
- 単体テストよりもリファクタリング耐性が大きく高まる

#### 単体テストでカバーできない範囲の信頼性

- フレームワークが実行するミドルウェア
- DBやクエリの実行
- 最終的なレスポンスの形式
- データの忠実性を
- テストを通して１つのDBが使用されるため1つのAPIに対して複数のテストケースの実行ができない場合もある
    - テストケース実行ごとに対象のデータをリセットするか、その分のテストは単体テストで補うかが必要
- 統合テストでも「設定」の変更をテストできない場合も多い
    - 設定ミスでバグが発生するというケースもそれなりにある点は留意が必要

#### 保守性（出来るだけ）

- テスト実行環境、テストデータを用意などで単体テストよりも仕組みが複雑になるので保守性が下がる
- テストケースが増えるほどデータ用意のコードが増え保守に苦労する
    - 担当プロダクトではここに改善の余地がある
- 統合テストの性質上、既存のものを変更するよりも今ある仕組みの上に新機能のテストを追加することが多い
    - 追加しやすくなるよう整える必要がありそう

## その他

- DBはモックせず統合テストで実行する、とはいえクエリの単体テストをできるようにしたい気持ちもある
    - これ良さそうで気になってる
    - [SQLを含めたユニットテストにgo-mysql-serverが便利](https://makiuchi-d.github.io/2023/08/12/go-mysql-server.ja.html)
    - MySQL互換のインメモリDB
- Usecaseが複雑になってしまった部分にどうにかテストを追加したいので、ここはやはりDBまでモックするのがいいんだろうな、という気持ちもある
- 統合テストの保守性については思案中なので良い案絶賛募集中です


## 参考文献

- 良かった書籍
    - [『単体テストの考え方/使い方』](https://book.mynavi.jp/ec/products/detail/id=134252)
        - 全ソフトウェアエンジニアに読んで欲しいくらい良い本でした、知りたいことが全て書いてあった
    - [『Googleのソフトウェアエンジニアリング』(11章テスト概観、12章ユニットテスト、13章テストダブル)](https://www.oreilly.co.jp/books/9784873119656/)
        - これもとても良かったです（拾い読みしてるので全章は読んでないです）
    - [『テスト駆動設計』](https://www.ohmsha.co.jp/book/9784274217883/)
        - テスト方針を決めるには少し物足りなかったので上記書籍や下の「変更容易性と理解容易性を支える自動テスト」などを読んでから実践編として読むのが適していそう
- t_wadaさんの資料
    - [変更容易性と理解容易性を支える自動テスト（2024/02版） / Automated Test Knowledge from Savanna 202402 YAPC::Hiroshima edition (speakerdeck)](https://speakerdeck.com/twada/automated-test-knowledge-from-savanna-202402-yapc-hiroshima-edition)
    - [【t-wada】自動テストの「嘘」をなくし、望ましい比率に近づける方法【Developer eXperience Day 2024 レポート】 | レバテックラボ（レバテックLAB）](https://levtech.jp/media/article/column/detail_496/)
    - [サバンナ便り〜自動テストに関する連載で得られた知見のまとめ〜 (speakerdeck)](https://speakerdeck.com/twada/automated-test-knowledge-from-savanna-202303)
    - [第3回 テストサイズ ～自動テストとCIにフィットする明確なテスト分類基準～ (技術評論社 サバンナ便り ～ソフトウェア開発の荒野を生き抜く～)](https://gihyo.jp/dev/serial/01/savanna-letter/0003)
    - [114. テスト駆動開発とは何であって、何でなかったのか？ w/ twada (fukabori.fm)](https://fukabori.fm/episode/114)
        - 技術系ポッドキャスト聴いたことなかったのですがfukabori.fm良かったです
- その他良かった記事
    - [単体テストの単位はコードではなく振る舞いである | azukiazusaのテックブログ2](https://azukiazusa.dev/blog/the-unit-of-unit-testing-is-behavior-not-code/)
    - [ふつうのユニットテストのための７つのルール｜koduki (Zenn)](https://zenn.dev/koduki/articles/106012fdd422fa67eae6)
    - [ユニットテストってもう言わない！ CI/CD時代のテスト分類に最適なテストサイズという考え方｜koduki (Zenn)](https://zenn.dev/koduki/articles/e0f8824adbe0e9)