良いテストの原則・向き合い方
業務でも個人でも、開発をする方は「自動テスト」を書くでしょう。
テスト嫌い、という方もいるかもしれないですが、やはりあるのとないのでは大幅に開発体験が変わります。
書く瞬間は面倒でも、後々の恩恵を考えると、もう大幅に違います。
という前提は、この記事では置いておき、テストを実際に書くことは思っているよりも難しいと感じます。以下のような観点で、いつも迷います。
- 何をテストすべき?
- どこまでテストすべき?
- 依存関係はどこまで考慮すべき?
- そもそもユニットテストは何をカバーするもの? etc...
正直、これらの問いについては答えがないです。言うなれば哲学です。
プロジェクトの思想や状況、言語によっても変わります。
しかし、迷ったときは、その原則・目的に立ち返ることで、妥当と思う判断がしやすくなるはずです。
そこで、good code, bad code という書籍の10章、「ユニットテストの原則」から、良いテストについて抽出し、自身の経験も踏まえてまとめてみようと思います。
良いテストとは?
以下の五つが、良いテストの特徴として挙げられています。
①:破損を正確に検出する
②:実装の詳細にとらわれない
③:良く説明された失敗
④:わかりやすいテストコード
⑤:簡単かつ迅速に実行する
一つずつ見ていきましょう。
1. 破損を正確に検出する
テストにおける最も明白な目的です。
本書籍では、
どんなに注意深くコーディングをしても、ある程度の間違いを避けることは、ほとんど不可能である
と述べられています。
テストを書くことで、そのコードが意図通り動くのか、信頼を得ることができます。
そして、上記はそのコードを書いているときの話ですが、その後の破損に気づけることも大きなメリットです。
コードは、修正や機能追加など、自分だけが触るわけではないです。
後々追加した処理により、期待していた動作をしなくなる可能性は大いにあります。
その際、ちゃんとテストが落ちてくれれば、意図しない破損に気づけます。
テスト対象のクラスは一つだったとしても、求める振る舞いは複数あります。
修正したい振る舞いのみに着目し、他の箇所に新たにバグを入れてしまう、なんてことを防ぐために、破損に気づけるテストが重要です。
2. 実装の詳細にとらわれない
実装の詳細とは、僕なりに言い換えると、「どのように」実現するかです。
そして、テストで確認したいのは「何を」実現するか、の部分です。
極端な例ですが、与えられた数の2乗を計算するクラスを作成したとします。
その方法は、たとえばrubyで書くと、以下の両方で実現できます。
class SquareCalculator
def calculate(num)
num * num
end
end
class SquareCalculator
def calculate(num)
num ** 2
end
end
ここで、「*を利用し、与えられたものを2回かける」のか、 「**を利用する」のかが、「どのように」実現するのか = 実装の詳細で、「数の2乗を返す」というのが「何を」して欲しいのかになります。
もし、この実装の詳細までテストに入れてしまうと、
- テストを書くこと自体が煩雑になる
- 修正が億劫になる
などのデメリットがあります。
コードベースが大きくなってくると、リファクタリングをしたい場面が増えます。
その際、実装の詳細まで網羅してしまうと、かえって扱いづらいコードになってしまいます。
よって、「どのように」ではなく「何を」に着目すべきです。
3. 良く説明された失敗
先述の通り、テストはコードの破損を検知します。
その際、「何が違ったのか」ということがわかりやすいと、迅速な原因特定につながります。
例えば、
ケースA:「与えた3つの要素のうち、2つが返されること」だけを確認する
ケースB:
「与えた要素が条件Aであれば返ること」
「与えた要素が条件Bであれば返ること」
「与えた要素が条件Cであれば返らないこと」
をそれぞれ確認する
それぞれがあったとします。
Aの場合、1つだけしか返されない、とエラーが出ても、果たして条件A,Bどちらが?ということまでわかりません。
しかし、ケースBであれば、どちらの想定がダメだったのか、すぐにわかります。
そして、これは適切なコード分離などにも繋がってくることと思います。
4. わかりやすいテストコード
これは、3と被る部分があります。
テストコードの失敗は、コードの破損を示すこともありますが、意図して変更する場合もあります。
3の例を利用すると、「条件A」の場合は返却されないようにしたいとします。
その際、ケースAの場合、確かに一つ減ったけど、それが条件Aの時なのか、条件Bの時なのかはっきりしません。
しかも、条件Bの時に返却されなかったとしても、テスト自体は通過するので気づかない場合さえあります。
一方、ケースBの場合、条件Bが返らなくなった、ということがわかりやすいです。
「何を」の部分をはっきりとさせる、ということかもしれません。
5. 簡単かつ迅速に実行する
業務だと特にですが、テストを実行する機会は思っているより多いです。
というか、コードに手を加える = テストコードを実行するということになるので、実行しない日はないでしょう。
この時、毎回長時間かかるテストだったらどうでしょう?
コードは小さな修正だけど、テストで10分とるんだよな〜みたいな。
こうなると、どうしてもテストの頻度は落ちますし、だんだんとストレスになります。
プロジェクトとしても、一つのタスク完了までの時間が遅くなります。
上記は、実体験としてもそうですが、やはり重たいテストは煩わしくなります。
だから、内容はもちろんのこと、「実行速度」にも焦点を当てる必要があり、「良いテスト」の要素として入っているのでしょう。
最後に
上記が、「良いテスト」の原則です。
一番大事なのは、それを踏まえて「じゃあ、どういうふうに書けば良いの?」というところだと思いますが、これは書籍を読むこと、そしてそれを実践で試行錯誤していくことでだんだんとわかってくるような気がします。
身も蓋もないですが。
でも、どうしてもそうなのだろうと思います。
プロジェクトにあるテストコードがよく作られているなら、そこから得られるものも多いでしょうし、そうでないのなら、着実に実践していくことで、だんだんと感覚を掴めると思います。
(とはいえ、僕も大いに発展途上ですが。)
そして、一番避けるべきは、「書かない」という選択です。
状況的に仕方がない、という場合もあるので、これも絶対ではないですが、ただ、やはり書く意識があるとテストコードだけでなく、実装でも無理なことを避けたり、より良い責任分離などを意識でき、結果として良いコード設計につながります。
テストコードを書くことは、確かに面倒です。しかし、後々の面倒の方が遥かに大きいです。
とらわれすぎるのも良くないでしょうが、力を抜いて、良いテストを目指したいなと思っています。
株式会社シンシア
株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。