現在では、自動テストの有用性も広く認知され、自動テストを記述すること自体は当然のこととして認識されている現場が多いのではないでしょうか。
一方で、ただ闇雲に自動テストを記述するだけで、却って開発速度を低下させたり、自動テストが本来果たすべき役割を十分に果たせていないという場面も見聞きします。
この記事では、
- なぜ自動テストを書くのか
- 「良い自動テスト」とはどのようなものか
- 「良い自動テスト」を積み上げるにはどうすれば良いか
について、個人的な見解を記述します。
なぜ自動テストを書くのか 🔗
先に結論を述べておくと、
任意の開発メンバーが、任意のタイミングで、 システムの正しさを検証可能にすることによって、 変更に伴う検証コストを小さくすること
これが自動テストの目的である、というのが私の意見です。
前提として、ビジネス上の価値を生み出すのは、あくまでプロダクトです。 ユーザーがプロダクトを利用し、それに対して支払う対価こそが、第一義的な「価値」であると考えています。
そのため、刹那的に捉えれば、ユーザーに「対価を支払うに値する」と思ってもらえるのであれば、 自動テストの有無、もっと言うと、どのような検証プロセスを経て提供されたのかは、問題にならない場合がほとんどかもしれません。
では、直接的な価値を生み出すわけでもなく、何なら追加のコストが発生するにも関わらず、自動テストを記述する必要があるのはなぜでしょうか。
これを理解するには、昨今のソフトウェア開発プロジェクトにおいて、次の3点がほぼ必須であることを把握しておかなければなりません。
- ユーザーに「対価を支払うに値する」と思ってもらえるプロダクトを一発で生み出すのは非常に難しいので、試行錯誤を繰り返す必要がある。
- 一度価値あるプロダクトを作ったとしても、何もしなければ、競合する製品の登場や、動作させるために前提としている環境の変化 (e.g., デバイスやプラットフォームの仕様変更など) によって、その価値は相対的に減少していく。
- 必要な要件を、必要なスピードで開発するためには、開発者が複数人必要になる。
一度実装したソースコードに変更を加えるタイミングは必ず発生します。
変更を加えるのは、最初の実装を行った開発者と同じ開発者とは限りません。
その上で、変更を加えたことによって、もともと提供していた価値が破壊されることはあってはならないわけです。
そのため、ソースコードの規模が大きくなり複雑になればなるほど、変更にともなうコスト、より正確には加えた変更が既存の価値を毀損していないことを保証するためにかかるコストは大きくなります。
自動テストは、この課題に対する1つの解決策で、あると捉えます。 すなわち、
- 自動テストを全て通過すれば、変更が何も壊していないことの裏付けになる。
- 自動テストの実行においては、前提知識を求めず “実行ボタン” を押すだけで検証可能。
という状態を構築することで、変更に伴うコストを小さくできることが、自動テストの価値である、と考えています。
(※ 別の角度として、規模の拡大に伴う複雑さの増加を抑えるために、ソフトウェアアーキテクチャを洗練させたり適切なデザインパターンを駆使するなど、変更に強い設計を行うことも重要です。それはまた別の機会に。)
「良い自動テスト」とはどのようなものか 🔗
前章で、自動テストの目的を
任意の開発メンバーが、任意のタイミングで、 システムの正しさを検証可能にすることによって、 変更に伴う検証コストを小さくする
ことと定義したので、これにつながる自動テストが「良い自動テスト」ということになります。
「良い自動テストが書かれている状態」を際立たせるために、逆の状態、すなわち「悪い自動テストが書かれている状態」を考えてみます。
この状態とはすなわち、「自動テストは一定書かれているものの、変更に伴う検証コストが大きい」ということです。
考えうる(そして、実際によく見る)ケースとしては、
- 変更を加えた際に、自動テストそのものの大幅な修正が必要となった。
- コード変更が、仕様の変更に起因するものであればテストの修正は当然に発生するので、それ自体は問題ない。
- うまくいっていない例としてはは、たとえばリファクタリングを実施した際に、リファクタリング対象のコードとは無関係なテストが失敗するようになるケースなど。
- 自動テストが FAIL するものの、なぜ失敗しているのか特定できなかった。
- 言い換えると、そのテストケースが「何を検査しているのか」が明確ではない、ということ。
という状況です。
前者を「脆弱なテスト」、後者を「不明確なテスト」と呼ぶとすると、「良い自動テスト」とは、
- 堅牢である: テスト対象の振る舞いが変わるべきではない変更のみを加えている限りは、自動テストは何らの変更を加えなくても成功するべきである。
- 明確である: テストが FAIL したときに、その原因を容易に特定できるべきである。
という2つの点が重要であると考えています。
「良い自動テスト」を積み上げるにはどうすれば良いか 🔗
ここまでで、自動テストは、
任意の開発メンバーが、任意のタイミングで、システムの正しさを検証可能にすることによって、変更に伴う検証コストを小さくする
ために作られ、そのための自動テストは、
堅牢である: テスト対象の振る舞いが変わるべきではない変更のみを加えている限りは、自動テストは何らの変更を加えなくても成功するべきである。
こと、そして、
明確である: テストが FAIL したときに、その原因を容易に特定できるべきである。
ことが重要であると述べました。
「良い自動テスト」を積み上げるためには、
- テスト対象の検討: 何をテストすべきか
- テストの記述方法の検討: どうやってテストすべきか
を慎重に考える必要があります。
ここは全て書くと膨大になってしまうので、 一旦詳細は Googleのソフトウェアエンジニアリング (第11-13章) や テスト駆動開発 に譲って余裕ができた時に書きます。
意識すべきポイントは、以下だと考えています。
- 可能な限り、本番環境と同じ条件を用意する。
- 実際に接続する > Fake > Mock/Stub の順に検討する
- DRY より DAMP (Descriptive And Meaningful Phrases) であるべき。
- Cucumber などのように、構造化されたテストケースを記述する。
- テストにロジックを入れない。
- fixture にしろ assertion にしろ、if や for が必要になるということは、何かがおかしい。
- テストデータは builder でつくり、大事な値を際立たせる。
結びに替えて 🔗
これまでには、
- 「とにかくテストを書け」という圧力のもと、各メンバーが思い思いのテストを書いている。
- 「結合は疎でなければならない!」という思いから、とりあえずモックフレームワークが導入されている。
- 細かいテストの書き方や、それを統一/強制することばかりに注意が向いている。
- 「カバレッジこそ正義」と言わんばかりに、ひたすらカバレッジを追求する。
- 「testing library には何を使うべきか」という議論が先行する。
- そうして積み上がっていく、負債としてのテスト…
という場面に何度も突撃し、揉まれ、打ちのめされながら、最近ようやく「進捗に伴って、価値ある自動テストが積み上がっていく」という方向でプロジェクトをディレクションすることがちょっとずつできるようになってきた気がしてます。
(もちろん、look&feel の統一や、効率的な記述方法の追求は重要なことではあります。)
それがなぜなのかを考えた時に、
「テストダブルはどうしろ」だとか「テストピラミッドがどうだ」とか「Go は TableDrivenTest だ」とか、そういった方法論がチームに浸透したタイミングより、
「そもそも我々は誰のために、何のためにテストを書いているのか」といった根本的な自動テストの捉え方が浸透した時にうまく歯車が噛み合い始めることが多いように感じており、
最近、新たに立ち上がるプロジェクトがあり、「自動テストとはどうあるべきか」について情報発信する機会に恵まれたので、自分なりに言語化した結果を残しておきました。