【脱負債ユニットテスト】コミットされるべきユニットテストのテストケースについて考察する

## はじめに
### 概要
みなさんはコメントアウトされて何年間も実行されてない、
ゾンビの様なユニットテストを見たことがあるでしょうか。
私はあります。なんとも言えない気持ちになったことを今でも覚えていますw
今回は、せっかく書いたユニットテストが負債となってしまわないための
個人的なTipsをシュアさせていただきます。

尚今回の記事は完全に個人の考えなので、反対意見がある場合は
遠慮なくコメントをいただけますと幸いです。

### 対象読者
* 品質に興味がある方
* これから自動テストを始めたい人
* ユニットテストで何を確認すべきか迷っている人
* なんとなくユニットテストを書いている人
* ユニットテストの負債に苦しんでいる人

### 本記事でのユニットテストの定義
* 自動化されたテストであること
* DBや外部API、実行環境などの外部要因に依存しないこと
* xUnitなどを使ってコードベースで実装されていること
* クラスやコンポーネント単位の小さいテストであること

## なぜユニットテストのコミットに気を配るべきなのか
### 意図が不明瞭なユニットテストは負債になる
テストコードはどうしてもプロダクトコードに比べてメンテナンスされる機会は少ないです。
趣味の個人開発でもない限り顧客やビジネスへの影響が少ないため、
日頃から１つ１つのテストケースやテストスイート[^1]に自体に目が向けれることは少ないです。
また、テストコードはプロダクトコードよりも意図が伝わりづらいのも負債となりやすい要因の１つです。
プロダクトコードは抽象化された振る舞いが記述されているため具体的なケースが想像しやすいですが、
テストコードでは具体的なケースの記述がされており、そこから抽象化された仕様を読み解く必要があるのでどうしても理解するためのハードルが高くなります。

そのためコミットするユニットテストに関してはプロダクトコード以上に慎重になる必要があります。
あまりメンテされない＆＆意図がわかりづらいものになりやすいテストコードにおいては、
記述されるべきテストケースとその実装について精査されなければなりません。

負債となったテストケースは一向にパスできず、
むしろ機能追加の足枷になってしまうことも少なくありません。
加えて削除もしくはリファクタリングもどれくらい重要なのかがいまいちわからないため、
いじることもできず、残り続け負債が積み上がってしまうのです。

ではどの様なテストケースを書けばいいのかをこれから考察していきましょう。

## プロダクトはどの様なプロセスで開発されるべきなのか
あるべきユニットテストについて考えるためには、まず開発プロセス全体に目を向ける必要があります。
ユニットテスト自体ははあくまで開発の中の１つのプロセスに過ぎません。
あくまで我々エンジニアの目標は「バグが極力少ないプロダクトを、
可能な限り素早く市場に出し続けること」であり、
各工程はそれを達成するための手段の１つにすぎません。
そこで伝統的なソフトウェア開発V字モデルからユニットテストの役割を見て行きましょう。

### V字モデルから考えるユニットテストの役割
エンジニアであれば嫌というほど目にした方も多いであろう、有名な開発プロセスを示した図です。
![V字モデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1795116/a4ced635-d3dd-4c7c-b5dd-05c5307dc681.png)

ここからユニットテストと対応している工程は詳細設計であるとわかるはずです。
[こちらの記事](https://hnavi.co.jp/knowledge/blog/external_design/)によると詳細設計で決定される内部仕様（内部設計）では

1. モジュールごとの機能
1. 内部データの仕様
1. データの入出力に関する仕様

が決定されます。そのような「内部仕様を満たしていることを確認するためのテスト」が
ユニットテストなのです。

ここでいう内部仕様とは、あくまでprivateメソッドの様な機能の１挙動ではなく
publicメソッドの様な外部に公開されている機能単位であることに留意してください。
テストケースは内部的なふるまいに則って記述されるべきではありません。
その様なテストケースはプロダクトコードとテストコードのの蜜結合を引き起こし、
最悪の場合、変更があるたびにテストコードの修正が必要になるようになります。

### アジャイル開発のケースに置き換えると
アジャイル開発でもやっている手順は大まかには一緒なはずです。粒度の違いこそあれど、
`設計->実装->テスト`のイテレーションを細かく区切って実行しているにすぎません。
よって同様にこちらも粒度の差こそあれど、やることとその目的に違いはないはずです。
ただアジャイル開発ではドキュメントを詳細に残さない傾向にあるので、
小さい改修などでは詳細設計のドキュメントは残さないケースもあるかと思います。
その際はチケットでもローカルのメモでもいいのでどこかにメモしておくとよいでしょう。


## 負債にならないテストケースの条件とは
### ドキュメントとして機能すること
ここまでで開発プロセス全体からあるべきユニットテストの全体像を掴んできました。
今度はもう少し具体的にユニットテストとそれを構成するテストケースについて考察していきます。

負債とならない良いユニットテストは、
「内部仕様を満たしていることが確認できる」動くドキュメントとして機能します。
内部仕様がテストケースによって過不足なく網羅され、繰り返し検証されることによって
実行可能なドキュメントとして機能し、テスト対象について信頼できる情報を提供し続けることができるのです。
その様なユニットテストだからこそ、リファクタリングの際にも現状の振る舞いについての情報を提供でき、またデグレ[^2]のセーフティーネットしても機能することができるのです。

ではここからはユニットテストがドキュメントとして機能するために必要なルールについて考察していきます。

#### Ⅰ.　仕様がテスト可能な単位に分割されている
ドキュメントとして機能させる場合、ユニットテストの原則に則って仕様を作成する必要があります。
特に重要視すべきなのが [「１つのテストメソッドで確認すべき項目は１つのみ」](https://jisou-programmer.beproud.jp/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/20-1%E3%81%A4%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A7%E3%81%AF1%E3%81%A4%E3%81%AE%E9%A0%85%E7%9B%AE%E3%81%AE%E3%81%BF%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B.html)という原則です。
これは複数の項目が１つのテストメソッド[^3]で検証され、エラーが起きた際に
原因の特定が困難になることを避けるためのルールです。
文章でのみ設計がおこなわれるとどうしても複数の項目を１つの仕様として扱ってしまいそうになるので、テストを作成しながら適宜仕様の修正していくと良いと思います。

#### Ⅱ.　仕様に対してテストケースが存在している
テスト対象のドキュメントに記載されているべき情報は、テスト対象の仕様とそれに関するユースケース（シナリオ）です。よって各テストケースはシナリオを用いて仕様を満たしていることを帰納的に確認できることが求められます。テストケースが正しく動作することにより、テスト対象に関する信頼できる情報を提供し続けることができ、デグレが発生した際に原因を即座に確認できるセーフティネットとして機能する理想的なユニットテストを残し続けることができます。

#### Ⅲ.　１つの仕様に対して１つのテストメソッドを心がける
境界値テスト[^4]などをユニット実施する場合、仕様に対して複数のテストメソッドを作成したくなりますが、個人的にはこれも避けた方が良いと思っています。しっかりテストしようとするとユニットテストはプロダクトコードに比べるとボリュームが多くなってしまいがちですが、これはユニットテストの全体の可読性が下がり、結果としてドキュメントとして機能しなくなってしまいます。

テスト対象のメソッドとテストメソッドの1:1の関係を心がけることでテストコードのボリュームを抑えることができます。もし複数のデータを用いて確認したい場合は[Parametrizedテスト](http://xunitpatterns.com/Parameterized%20Test.html)をテスティングフレームワーク[^5]を駆使して一つのテストメソッドで実行できる様に工夫すべきですどうしても無理な場合はファイルをテストケースの特性ごとに分割するのも手ではあると思いますが、極力避けた方がいいと思います。

#### Ⅳ.　テストメソッドの意図を明確に伝える
我々は日常的に自然言語[^6]を使って生活しているので、ソースコードを読むよはるかに少ない労力で内容を理解できます。よってテストケースの説明はソースコードではなく自然言語で行われるべきです。そのためにテストメソッドはテストの意図が即座に理解できる名前をつけるべきです。複数のシナリオを１つのテストメソッド内に含め、普遍的で何を確認したいのかわからない名前になってしまったら要注意です。

またテスト対象の仕様が複雑で複雑な前提条件が必要な場合は、メソッドドキュメント[^7]を残し意図を簡潔に伝えられる様にすべきです。テストメソッドはプロダクトコード以上に記述量が膨れることも少なくないので、何を確認したいのか、前提はどうなのかを言葉でまとめることで保守される際に長いコードを読まなくて済む様な仕組みが必要です。

#### Ⅴ.　ロジックがシンプルであること
ユニットテストでは１つのテストケースで入力データの作成やその後片付けなどやるべきことが多くあり、記述力が膨れがちになり可読性が悪くなります。そうならないために `AAA構文（Arrange/Act/Assert）` や `GWT構文（Given-When-Then）`を意識してロジックを組みたてましょう。

これらの構文は各テストケースで行われるべき `準備/実行/検証` をきちんとフェーズに分けて記述するためのフレームワークです。この流れに従ってロジックを描くことによって、どこのフェーズで何をしているのかが明確にわかり、ロジックに間違いがないことも後から容易に確認することができます。意図が伝わりづらいロジックはドキュメントとしてのテストに悪影響なので必ずこれに従って記述しましょう。

また言語によってはテスティングフレームワークに上記の構文がサポートされていないこともあるので、コメントを活用して構成を守るようにすると良いでしょう。


### その他のユニットテストの原則について
他にもユニットテストを書く際に重要な原則が下記の記事で紹介されているので、
気になった方は是非併せて読んでください。

https://qiita.com/hgsgtk/items/a3186a250d36d3b224d9

https://myenigma.hatenablog.com/entry/2022/04/24/143753#Unit-test%E3%81%AF%E9%80%9F%E3%81%84%E3%81%BB%E3%81%86%E3%81%8C%E8%89%AF%E3%81%84

https://qiita.com/hgsgtk/items/00daa278516d1995dac6


## まとめ
何をテストしたいのかわからないユニットテストは、
テスト全体の可読性の低下と信頼性の低下を招きます。
そのため確認されるべきことが過不足なく網羅されている状態があるべき姿であると考えます。
仕様からテストケースを作成することで、何をテストしているのかを明確にすることが
負債にならないユニットテストの絶対条件であると私は考えています。


## 参考文献
### 書籍
* [実践 JUnit - 達人プログラマーのユニットテスト技法](https://www.oreilly.co.jp/books/9784873117300/)

### 記事・スライド
* [良いUnit Testの書き方](https://myenigma.hatenablog.com/entry/2022/04/24/143753#Unit-test%E3%81%AF%E9%80%9F%E3%81%84%E3%81%BB%E3%81%86%E3%81%8C%E8%89%AF%E3%81%84
)
* [ブラックボックステストとホワイトボックステスト](https://dev.classmethod.jp/articles/blackbox_testing_and_whitebox_testing/)
* [リーダブルテストコード](https://speakerdeck.com/jnchito/number-vstat?slide=40)
* [xUnit Test Patternsから学ぶ12個のユニットテストの原則](https://qiita.com/hgsgtk/items/a3186a250d36d3b224d9)
* [xUnit Test Patternsから学ぶユニットテストの６つの目指すべきゴール](https://qiita.com/hgsgtk/items/00daa278516d1995dac6)
* [xUnit Test Patternsから学ぶテストアンチパターン](https://speakerdeck.com/hgsgtk/testing-anti-pattern-learned-in-xunit-test-pattern)
* [今度こそユニットテストを書き始めるために](https://zenn.dev/koduki/articles/93ebc677493e7a)
* [基本設計・詳細設計とは？仕様書との違いは？企業の設計課題を解決する方法](https://products.sint.co.jp/obdz/blog/basic-design-detailed-design)
* [自走プログラマー【抜粋版】 1.4. ユニットテスト](https://jisou-programmer.beproud.jp/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/index.html)
* [「外部設計」と「内部設計」とは？それぞれの違いと作業内容を解説](https://hnavi.co.jp/knowledge/blog/external_design/)
* [「質」のいいユニットテストを書くためのプラクティス](https://speakerdeck.com/hgsgtk/practices-to-write-better-unit-test?slide=63)


[^1]:　複数のテストケースをまとめたもの。
[^2]:　デグレードの略語。プログラムの修正・変更により、バグの発生など品質が低下すること。
[^3]:　テストケースがプログラムとして記述されたメソッド。ユニットテストの動作の最小単位。
[^4]:　仕様条件の境界となる値とその隣の値に対してテストする技法
[^5]:　Junit, RSpecなどのユニットテストの実行・結果の判定/集計/レポートなどの機能を提供するフレームワーク
[^6]:　人間が生活のなかで普通に使っている（日本語や英語のような）言語。
[^7]:　各メソッドの上部に記述されるドキュメントコメント