「テストを書く」ことは当たり前になってきましたが、良いテストと悪いテストを判別する審美眼を磨く話はあまり語られません。
その審美眼を磨く第一歩として、ユニットテストが何を目指すべきなのかを考えていきます。
1. ユニットテストが目指すもの
ユニットテストが目指すものは単純明快です。
プロジェクトの持続的な成長を可能にすることです。
当然と言われれば当然ですが、意外と意識していないプロジェクトも多いのではないでしょうか。
- テストを書けと言われたから書いている
- TDDが流行っているから書いている
- とりあえずテストを書いて品質を担保している
- 世間的な流れで書いている
といった方も多いです。
なので今回はまず、「ゴールを決める」ことで、逆算し、ユニットテストの効力を最大限に発揮する第一歩を踏み出します。
1.1. プロジェクトの持続的な成長を可能にすること
プロジェクトの持続的な成長を可能にすることとは具体的に何を満たす必要があるのでしょうか。
- セーフティネットとしてのテスト
- ドキュメントとしてのテスト
- 設計技法としてのテスト
簡単に言ってしまえばこの3点になります。1つ1つ深く見ていきます。
1.1.1. セーフティネットとして
セーフティネットとしてのテストですが、一体何からのセーフティネットなのでしょうか。
それはリグレッションからです。
リグレッションとは…コード修正などを行なった後に、ある機能が意図した通りに動作がしなくなること。バグと同義語。
セーフティネットがないとコードの変更や追加は非常にリスクの高い行動となってしまいます。
- 「少しコードをリファクタリングしたらある処理を忘れてバグが発生した」
- 「新しい機能を追加したら、それが原因でバグに繋がった」
- 「機能を追加することになったが、すでに書かれているところを触るのは怖いので触らない」
セーフティネットがない限り、コードはどんどん劣化してい、気がついた時には手が付けられないほどのメンテナンスコストが掛かってしまう負債となってしまうのです。
そんなセーフティネットとしてのテストのデメリットですが、【プロジェクトのスタート段階で多大な労力を要する】 ことです。
しかし、このようなデメリットは長期的な目線で見ると安いコストになります。
テストのないプロジェクトのスタートは早くても、プロジェクトが進行すればするほど、指数関数的にメンテナンスコスト(必要時間)が高くなっていきます。
そして、これは悪いテストを量産している場合にも同じ事が言えます。
なぜなら悪いテストは「手がかかる」からです。
多少のリファクタリングによってテストが落ちたり、可読性が低く、テストが理解しにくかったり、実行が遅くて気軽に実行ができなかったりなど理由はさまざまです。
とにかく「手がかかる」ことによって、プロジェクトの進行とと共にメンテナンスコストも肥大化してしまうのです。
1.1.2. ドキュメントとして
プロジェクトの持続的な成長を可能にするためにはテストをドキュメントとして使えると理解がスムーズになります。
たとえば、適当なOSSを読もうと思った時にOSSがどう動作するか理解するには以下の通りです。
- 実際のアプリを自分で動かして確認する
- ソースコードを改変しつつ確認する
しかし、ここでドキュメントとしてのテストが書かれていれば、一目でアプリの使用するシナリオが理解できます。
また、1つの動作単位に関連するクラス等も一眼て理解できる為、どのクラスがどのクラスに依存しているかが明らかであり、手を加えやすいということです。
サンプルとは言え、データの形式(型)もわかるので、より理解が進みます。
ドキュメントとして整えられたテストがあるプロジェクトは、成長する土台が整っているとも言えます。
1.1.3. 設計技法として
ユニットテストが簡単にできるかどうかで、質の低いコードを高い精度で指摘できます。
ユニットテストが困難なコードは改善が必要であることを示していて、結合度が強いです。
しかし、逆は言えません。ユニットテストが簡単にできるから良いコードではないのに注意してください。
どっちにしろ、テストが簡単にできないコードは質が低い傾向にあるので、良いリトマス試験紙としてテストは大活躍します。
1.2. よくあるアンチパターン
意外とやりがちなアンチパターンをいくつかあげます。
1.2.1. カバレッジ数値を目指してしまう
このセクションにおけるカバレッジは、コードカバレッジとブランチカバレッジのどちらも指します。
カバレッジはテストを網羅しているという指標にはなるのですが、単純に信頼できません。
なぜなら、コードの書き方によって大きくカバレッジが変わってしまうからです。
でもカバレッジが低すぎるのは問題です。テストが十分に行われていないからです。
カバレッジは行数だけに焦点を当てている為、極端な話アサーションを含まないテストであっても高カバレッジ数を出すことができます。
つまり、高すぎる目標カバレッジ率は抜け道を探させ、品質の確保よりもカバレッジ率を上げるために注力してしまうからよくないのです。
もし「このプロジェクトではカバレッジ率100%にしてください」と言われたら、すべてにテストを書く事が必須になるのでコストが増えます。
さらにカバレッジを上げなきゃいけない上に、テストは評価されづらいのでテストケースの量が少なくなります。高カバレッジ率だったとしても、テストケースが少ない場合も往々にあります。
また、カバレッジ率は外部ライブラリのことは考慮されていません。結局相対的な数字にしかすぎないため、人間の目で確認する他に良い方法はないんです。
1.2.2. 使い捨てのテストを書いてしまう
- 可読性が低いテスト
- 保守性が低いテスト
- 巨大なテスト
- 複雑なテスト
1.2.3. 可読性が低いテスト
テストは使い捨てだからだと考えて、変数名が適当だったり、冗長な書き方をしていませんか。
後々仕様に変更があり、修正しなければいけない時に可読性が低いテストだとメンテナンスコストが大きくあがってしまいます。
メンテナンスコストが上がってしまうとメンテナンスされなくなる危険性が出てきます。
あなたは他の人の書いた読みにくいテストコードをメンテしたいと感じる場合は別です。
1.2.4. 保守性が低いテスト
実装の内容に結びつきすぎているテストを書いていませんか。
たとえば、アルゴリズムをそのまま書き写していたり、ちょっと変数名を変えたら修正が必要なテストコードを書いていたりなどです。
そのように保守性が低い・リファクタリング耐性のないテストコードはメンテナンスされなくなる可能性を含んでいます。
1.2.5. 巨大なテスト
1つのテストメソッドの中に複数のテスト事項を書いていませんか。
テストは事前データを作成するのが面倒です。複数のリレーションがあると、すべて事前データを作成する必要が出てくるのでシステムを理解する必要が出てきて大変です。
それでも1つのテストメソッドに複数のテスト事項を埋め込むのは辞めるべきです。
テストが落ちた時に何が原因か分かりにくくなりますし、可読性も落ちます。
1.2.6. 複雑なテスト
テストコードにif文を使うのは明確なアンチパターンです。
if文がテストコードに入るだけで格段と読みにくく、理解しにくいテストコードになります。
if文を使う時点でメソッドを分割して別事項としてテストすべきです。
1.2.7. 開発サイクルで使われていない
自動化テストの最大の利点は常に使用されることです。
理想的には、どんなに小さな変更であったとしても、すべてのコード変更時にすべてのテストを実行すべきなのです。(リグレッションからの保護)
1.2.8. すべてにテストを書く
アプリケーションでもっとも重要な部分は、ビジネスロジックを含む部分、ドメインモデルです。
ビジネスロジックをテストすることで、コスパの良いテストを書けます。
ドメインモデル以外にもインフラに関する部分のテストは書くべきですが、コードすべてにテストを書く必要はないです。
逆に、すべてのコードに対してテストを書いてしまうと、リグレッションからの保護が対して利益を得られないのにもかかわらず、変更のコストだけが大きく膨れ上がってしまう状況になることがあります。
1.3. まとめ
- プロジェクトの成長を手助けするテストコードが理想
- メンテナンスコストの高いテストコードは書かない
- テストコードは使い捨てではなく、メンテナンスされていくものだと考える
- つまり、本番コードと重要度は等価と考える必要がある
- カバレッジ率をあげることに注力しても品質は上がらない
- テストは修正ごとに行い、理想はすべてのテストを動かすこと
- 予想外の依存関係から発生するリグレッションをテストするため