今回、Software Design 2022年3月号 第2特集
第1章では、混同されることの多い自動テスト関係の概念を、自動テスト、テストファースト、テスト駆動開発の3つの段階に分け、それぞれの効果や注意点を説明します。ソフトウェアを継続的に変更/
はじめに:混同されやすい自動テスト関係の概念
開発者がコードに対してテストコードを書く
テスト駆動開発
本章では、混同されることの多い自動テスト関係の概念を、自動テスト、テストファースト、テスト駆動開発の3つの段階に分け、それぞれの効果や注意点を整理整頓していきます。
自動テストとは何か
自動テストとは、開発対象のコードに対するテストもコードとして書いて
def test_sizeメソッドは現在の要素数を返す():
stack = Stack()
stack.push('foo')
assert stack.size() === 1
自動テストに必須の性質
自動テストの強みの源となる、自動テストが必ず満たすべき性質が2つあります。自己検証可能であることと、繰り返し可能であることです。
Self-Validating(自己検証可能)
「自己検証可能」
Repeatable(繰り返し可能)
「繰り返し可能」
自動テストが持つべき性質
次に、必須とまでは言いませんが、自動テストに強く推奨される性質が2つあります。独立していることと、高速であることです。
Independent/Isolated(独立している)
自動テストは互いに独立していることが望ましいです。
Fast(高速である)
自動テストは可能な限り高速に動くことが望ましいです。テストの実行が速いと今自分が考えたとおりにコードが動いているかどうか一瞬でわかるようになります。高速でないと実行頻度が下がり、頻度が下がると問題の把握が遅れ、その結果じわじわと自信が減っていき、コードの変更に躊躇するようになってしまいます。
自動テストの効果を高める因子
より自動テストの効果を高める因子があります。誰が、いつ、どのくらい書き、どのくらいの頻度で実行するかです。
誰が書くか
テスト対象のコードを書く本人が自動テストも書くと、効果が高まります。実装を書いた本人こそが、自動テストがもたらすフィードバックを実装に反映するハードルが最も低いからです。
いつ書くか
自動テストは、書くタイミングがテスト対象のコードが書かれた時点に近ければ近いほど効果を発揮します。テストを書き、そのテストを動かすことから、設計と実装に対するフィードバックが得られるからです。
どのくらい書くか
カバーしている範囲が広がってくると、自動テストはさらに効果を発揮するようになります。カバー範囲が広がると、書いたコードが実装時予想しなかった部分を壊してしまうような事態に気づけるようになります。テストカバレッジを数値目標にすると手段の目的化が起こりがちなので注意が必要ですが、理想的なのは
どのくらいの頻度で実行するか
自動テストは頻繁に実行するとさらに効果を発揮します。頻繁に実行することで、コードが動かなくなった瞬間、つまり欠陥が入った瞬間をとらえられるからです。
自動テストの理想的な状態
要するに、自動テストの理想は、テスト対象を実装する本人によって実装と近いタイミング
自動テストの効果
自動テストにはさまざまな効果があります。すぐに発揮される効果、しばらくしてから発揮される効果、組織やチームに対して現れる効果などです。それらをまとめていきましょう。
すぐに発揮される効果
自動テストには、書いたそばからすぐに発揮される効果があります。
即時フィードバックが得られる
自動テストを書くと、今書いた実装コードが想像どおりに動くことをすぐに確認できます。できると思ったことが実際にできているという自信につながります。
自動テストを書くと、今書いたコードが想像どおりに動かないこともすぐにわかります。軽微なミスに、すぐに自分で気づけるので、安心感があり、自信の喪失を防いでくれます。
「やればできる感じ
デバッグを大幅に軽減する
自動テストがあると、今書いた実装コードがどこかを壊したらすぐに気づけます。このようなテストを回帰テストといいます。先ほどまで成功していたテストが失敗したときは、直前の変更で欠陥が入ってしまったとわかります。直前の変更内容であれば覚えていますし、すぐに直したり方向転換したりするのも容易です。
自動テストは、コードを書く時点で欠陥が入ることを防いでくれます。これによってデバッグの時間が圧倒的に削減されます。欠陥混入率の低下による圧倒的な生産性向上、これは非常に大きなメリットです。
テスト対象の理解が深まる
自動テストでは対象のコードが具体的にどう動くべきなのかを記述していくので、テスト対象をより広く深く考えるようになります。具体例を考える過程で、仕様のあいまいさや潜在的な欠陥にも気づきやすくなります。
設計改善のきっかけになる
実装とテストのタイミングが近いと、テストが設計改善のためのフィードバックとして働きます。
実装者本人が実装と近いタイミングでテストを書くと設計変更のハードルが下がり、現状の実装に対して無理やりテストを書くのではなく、実装のほうを、テストを書きやすい設計に変更できます。テストを書きやすい設計とは、
しばらくしてから発揮される効果
自動テストには、すぐに発揮される効果だけでなく、しばらく時間が経ってから発揮される効果もあります。
記憶力や把握力の限界を補う
人間の記憶力は頼りないもので、数週間前に書いたコードのことすらよく覚えていません。
自動テストが書かれていれば、少なくとも当時の想定どおりには、今この瞬間にもソフトウェアが動いていることを、ごく短い時間で確認できます。自動テストが人間の記憶力と把握力の限界を補ってくれるのです。
生きた詳細なドキュメントになる
ドキュメントでよく問題になるのが、実際の動きとの乖離です。たくさんの変更がソフトウェアに入るうちに、ドキュメントの更新が追いつかなくなってきたのでしょう。
テストコードは実行可能で詳細なドキュメントになります。まず、内容の乖離に気づけるようになります。ドキュメント対象の動きが変わったらテストが失敗するからです。そして、
組織、チーム、プロジェクトに発揮される効果
自動テストには、個人だけでなくチームに発揮される効果もあります。
属人性を軽減する
自動テストを書くときに使うJUnit、Jest、pytestなどのテスティングフレームワークによって、テストの書き方と実行方法が共通化されます。誰がいつ書いたテストコードであっても、読み、レビューし、編集し、実行できます。
プロセスで品質を作り込む助けになる
自動テストは、開発の過程で品質を作り込む効果があります。最後にテストして品質を判定するのではなく、最初から高い品質を常に保てるようになるのです。
自分でテストを書くと、自分が書くコードに対して責任感を持つようになります。新規機能を開発する際は、テストを書くことで安定した開発が可能になります。不具合が発生したときは、その不具合を再現するテストコードを書き、そのテストが成功するように修正を行うことで回帰テストもそろいます。コードレビューの際には、正常系だけでなくエッジケース
メトリクスが取れる
自動テストは良くも悪くも量を測れるので、チーム開発のメトリクスになります。テストのカバー範囲
結合、デプロイ、リリースの判断を支える
理想的な状態の自動テストは、システム全体を
システムが想定どおりに動いていることがYes/
コストメリットがある
一般的に欠陥のデバッグや修正にはコストがかかります。そして欠陥の混入から時間が経過すればするほど、欠陥の修正コストは上がっていきます。その逆も真です。欠陥が混入してから、それを見つけて修正するまでの時間が短ければ、圧倒的にコストが削減されます。
きちんと整備され、実行され続ける自動テストは、プロジェクトの期間を通じて欠陥修正やデバッグコスト削減のための効果を発揮し続け、大きなコストメリットを生みます。
最大の効果は「根拠ある自信」
開発しているシステムが自分たちの想定どおりに動いていることが短い時間でわかると、自信が生まれるようになります。ここまでさまざまな効果を説明してきましたが、根拠ある自信こそが、自動テストの最大の効果です。
自動テストが整備されていなければ、もっと良い設計が浮かんだり、改善
自動テストが整備されていれば、考えるとおりに動くことが確認できて、動かなくなったらすぐにわかり、誰のコードでも同じように編集できます。そこからは、いつでもどこからでも変化に適応できるという自信、改善に着手しようという勇気が生まれます。自動テストが生み出す、根拠ある自信と勇気が長期的で継続的な変更と改善を支えるのです。
自動テストの注意点
学習コストがかかる
さまざまな事例から、テストを書くコストはデバッグの大幅な軽減などで相殺され、開発速度の面ではむしろ黒字になることがわかっています。テストを書き慣れているプログラマーは、テストを書きながら開発するほうが速いことを知っています。
しかし、テストを書き慣れていないプログラマーは、テストコードの書き方を学びながら開発するので時間がかかってしまいます。
ソフトウェア開発において質とスピードはトレードオフではなく、質が上がるとスピードも上がり、質が落ちるとスピードも落ちるのが現実です。そのとき、スピードおよび質とのトレードオフになるのは、教育、学習、新しい技術への投資です。つまり、テストの書き方を学ぶには時間がかかります。
実装から時間が経過するとテストを書く難易度が上がる
実装から時間が経過すると、テストを書く難易度が上がります。実装したときのことはもう忘れていますし、テストのことを考えて書かれてこなかったコードは、そもそもテストを書きにくい構造になっていることが多いからです。
書籍
実装から時間が経過すると腰が重くなります。
現状の追認になると効果が薄い
「実装とテストのタイミングが近いとテスト対象の設計を改善するきっかけになる」
実装から時間が経過していると、すでにそのコードが本番投入されていたりほかの人が使っていたりして、設計変更のコストが上がります。すると、設計改善よりも現状維持のほうが選択されやすくなります。
進化した現代のテスト技術を使えば、テストが難しかろうと無理やりテストを書くことは可能です。テストが増えていけば、現状把握の度合いは深まりますし、回帰テストが整備されるという大きな効果もあります。しかし、テスト対象の設計や実装を変更しないなら、ソフトウェアの設計自体は改善されていないという点に注意が必要です。そのような現状追認の自動テストは動作を保証しているだけで、良い設計を保証しているわけではないのです。
メンテナンスコストがかかる
外発的なもの
変更を後押しするべき自動テストが変更の阻害要因になってしまうのでは本末転倒です。変わり続ける要件に適応していくには、テスト対象のコードだけでなく、テストコード自体のリファクタリング
構造的結合に注意
テスト対象の構造に強く依存したテストを書かないように注意しないと、自動テストと実装コードとの結合度が高まる傾向があります。これを構造的結合と言います。構造的結合の度合いが高いテストを書いてしまうと、実装の変更や改善の際にテストコードのメンテナンスも必要となり、改善の足かせになってしまうことが多いです。改善を後押しするべき自動テストが改善の足かせになるのは本末転倒です。現状追認のテストコードや、カバレッジ向上が目的のテストコードを書いているとき、良かれと思って構造的結合を高めてしまうことがあり、注意が必要です。
品質保証としてはもの足りない
自動テストは欠陥を減らし品質を高めることを後押ししますが、それによって品質が保証されているとか、欠陥はないという誤った安心感を持ってしまうことにも注意が必要です。
「最も効果が高いのはテスト対象コードを書いた本人がテストコードを書くときである」
テストファーストとは何か
先ほど、
テストファーストの効果
実装よりも先にテストを書くというのは一見奇妙なアイデアに思えますが、実際に書いてみるとそこには大きな効果があります。
必ずテスト可能なコードになる
先にテストを書くということは、テスト対象のコードは必ずテスト可能になるということです。実装時から時間が経過するとテストを書くのが難しいという問題がありましたが、テストを先に書くと、テストが書けない実装はできません。これは単純ですが非常に強力な制約です。
テスト可能なコードを書くにはテストファーストが最も有効というよりは、そうでもしないとテストは書けない、というのが現実的なところです。あとからテストを書くのが難しく、面倒なら、結局書かれないことが多いからです。テストファーストは、テスト可能なコードにたどり着く最も効果的な方法なのです。
テスト可能なコードを追い求めるとテストの書きやすい設計を意識することになり、疎結合で高凝集の設計に自然と導かれ、試験性
テストが書ける、そろう。これはテストファーストの大きな効果です。
設計改善効果が高い
テストから先に書くとは、テストから先に考えるということです。するとそこからはさまざまな設計改善の効果が生まれてきます。
インターフェースと実装を分けて考えられる
テストファーストでは
インターフェースと実装を分けて考えるのが設計では非常に大事ですが、テストファーストでは実装がそもそも存在しないので、自然とインターフェースから考えることができます。
外部からテスト対象を客観的にとらえることができる。これもテストファーストの大きな効果です。
利用者の視点に立った設計を導く
テストコードはテスト対象の最初の利用者になります。
ソフトウェア設計において、実装者にとっての作りやすさと利用者にとっての使いやすさ、わかりやすさは一致しないことが多々あります。ではどちらが中長期的に重要かというと、使いやすさやわかりやすさのほうが重要です。ソフトウェアは書かれる時間より読まれたり使われたりする時間のほうが、はるかに長いからです。先に実装を作ってから使うという順番では、すでに書いてしまった実装を使うようにバイアスが働いてしまいますが、テストファーストではそもそも存在しない実装を先に利用することで、利用者の視点に立てるのです。
利用者の視点に立った設計を導く。これもテストファーストの大きな効果です。
テストファーストの注意点
設計しすぎのムダ(スコープクリープ)に注意
皮肉なことですが、テストを書くタイミングが実装を追い越し、テストが設計としての意味を強く持ち始めることによって、先行設計の注意点がテストファーストにも現れます。
「こういうこともあろうかと」
修正性(変更容易性)にはあまり関与しない
自動テストの注意点と同じく、テストが書けているからといって良い設計であることが保証されてはいません。まだ足りないものがあります。
ソフトウェアの保守性の品質副特性[2]でいうならば、テストファーストは試験性
テスト駆動開発とは何か
テスト駆動開発
イテレーティブな開発手順(繰り返しながら作る)
TDDは次のような
- やるべきことをざっと整理し、箇条書きのTODOリストのような形で書き出しておく
- レッド:TODOリストから
「1つ」 ピックアップして、テストから書き (テストファースト)、そのテストを実行して失敗させる - グリーン:失敗しているテストを成功させることに集中した実装を行う
- リファクタリング:すべてのテストが成功しているままで実装コードやテストコードをきれいに整理整頓する
- リファクタリングが終わったら気づきをTODOリストに反映し、次のTODO項目を選んでレッドに進む
上記のように、レッド、グリーン、リファクタリング、レッド、グリーン、リファクタリング、......と繰り返していくのがTDDの姿です。なお、TDDを実際にどう行うかは第3章をご覧ください。
インクリメンタルな設計(少しずつ作る)
TDDにおいては、一度に1つだけのことを行います。最初にすべての設計を終わらせてから実装に入るのではなく、小さく安全なステップを繰り返しながら、設計も、実装も、テストも、すべてを1つずつ積み上げていくように開発していきます。そして、必要だと思うものから順番に着手していくことで、
このようにTDDは、開発を単一の目的の小さいステップに分解して手順を定義し、テストファーストとリファクタリングを技術的な基盤に据え、イテレーティブな手順とインクリメンタルな設計を組み合わせた、無駄がなく規律のある反復型のプログラミング手法です。
テスト駆動開発の効果
プロセスの強さ
TDDのメリットは目の前への集中です。
TDDのサイクルでは、各ステップで何をやるべきかがはっきりしています。自分がいるステップを認識していれば、今自分が何をすべきかが明確になります。
TDDは
保守性を高めるリファクタリング
リファクタリングはTDDの柱です。試験性
リファクタリングは非常に重要な技術ですが、自動テストと同じく
楽しく、誇らしい
TDDのプロセスに沿って進むとき、小さな挑戦と小さな成功の連鎖が短い時間で連続して訪れます。TDDはプログラミングをゲームのような、スポーツのような、常に達成感を感じられるアクティビティに変えてくれます。
また、リファクタリングが常に行われるので汚いコードを汚いまま放置しません。自分が書いているコードに対して常にプログラマーとして誇りを持てるのは、無視できない効果です。
「身軽であることが備え」
ソフトウェアが絶えず変化にさらされる現代において設計に終わりはなく、常に設計し続ける必要があります。
変化に備えすぎると予想を外したときの影響が大きいので、TDDは変化に備えすぎるのではなく、そのときどきで必要十分かつシンプルな設計を維持することに努めます。
TDDは必要十分でシンプルなコードを維持することで、今の最適な設計から次の最適な設計へとリファクタリングで安全かつスムーズに移行し続けることを狙っているのです。TDDは結果としての良い設計ではなく、良い設計の生成プロセスに注目していると言い換えてもいいでしょう。TDDの提唱者Kent Beckによれば、良い設計とは
TDDは、良い設計が生まれるフィードバックと改善のプロセスをプログラミングのステップに組み込むことで、答えに近づいていけるしくみだと言えるのではないでしょうか。
テスト駆動開発の注意点
TDDのメリットは
TDDではサイクルを円滑に回すために大きい問題を小さい問題に分割し、小さい問題を解決するテストとコードを組み合わせて大きな問題を解決していきます。その過程で、手前にゴールを設定するために、最終的には使わなくなるかもしれない小さな機能や設計、それらのテストを重ねていくことがあります。リファクタリングを怠ってこれらの過渡的な設計成果物としてのテストとコードが残り続けるとメンテナンスコストが上がってしまいます。
おわりに
ここ10年ほどで、ソフトウェア品質の中でも
試験性
したがって、現代において、変化に適応し続け、競争力のあるソフトウェアとチームを作るためには、自動テストは必須であると言えるでしょう。自動テストを書くのが有利なのではなく、書かないのが不利になる時代になりました。
本章では自動テスト、テストファースト、テスト駆動開発の3つの段階に分けて説明しましたが、この中では、自動テストが最も重要です。実装と自動テストをほぼ同時に書くのは個人にもチームにも大きな効果があります。
それに対して、テストを先に書くか
本特集を読んで、自動テストとテストファースト、テスト駆動開発を判別できるようになり、また本特集が自分の開発スタイルとして取捨選択を行うきっかけになれば幸いです。