精読「ソフトウェア品質を高める開発者テスト 改訂版」
ソフトウェア品質を高める開発者テスト 改訂版 アジャイル時代の実践的・効率的でスムーズなテストのやり方
アジャイル開発に即した効率的で実践的なテスト手法を学べる一冊です。ユニットテストやTDD、CI/CD統合まで幅広くカバーし、初心者からベテランまで活用可能。テストを通じて品質向上を目指すすべての開発者におすすめです。
はじめに
上流品質
上流品質は、つまり設計やコーディング段階で品質を高める「Shift Left」である。(アジャイルで品質を担保すること)
Shift Leftより
アジャイルでの品質
アジャイルの品質は、開発者とテスト担当者が協力し、短いサイクル内でバグを早期発見・解決することで担保される。Scrumの理念に基づき、マルチ学習や役割分担、データ駆動型の定量評価が重要。システムテストも自組織で活動内容を定義し、効率的に進める必要がある。これにより、迅速かつ信頼性の高い開発が可能になる。
上流品質向上のためのテスト
上流品質活動
上流品質を向上させるためには、以下の活動が重要
- 要求仕様やユーザーストーリの明確化
- クラスや関数構造の簡素化
- 単体テストや統合テストの実行
- レビューの実施
さぼる・逆らう人のための上流テスト講座
上流工程で85%以上のバグを発見することで、スケジュール遅延や出荷後の致命的バグを大幅に減らせる。統合テストやシステムテストではバグ検出率に限界があり、後半工程での修正コストは上流の数倍に増加する。そのため、レビューや単体テストを重視する「Shift Left」アプローチを採用し、開発初期にバグを摘出することが品質向上とコスト削減の鍵となる。
開発者テストの基本の基本
単体テストは重要とされながら、実際には形だけになっているケースが多く見られる。テストケースが明文化されていない、設計書が未整備などの問題が典型的。
単体テストでは、境界値や組み合わせテストを取り入れるなど、質の高いテスト手法が必要。また、クラス図やシーケンス図の活用で設計を明確にすることも効果的。
全体の品質計画を考慮し、適切なテスト戦略を設計することが、ソフトウェア開発の成功につながる。
開発者がこれだけは知っておくべきテスト手法
単体テストには以下の3つの手法を理解・実践することが重要
-
境界値テスト
バグは多くの場合、境界で発生する。例えば、「1から999の範囲」と指定された場合、1や999付近の値を詳しくテストすることで、コードの条件式ミス(例:>
と>=
の間違い)や境界処理の欠落を見つけることができる。この手法だけでも多くのバグを防げる。 -
状態遷移テスト
システムの状態とその間の遷移をモデル化し、正常・異常の動作を確認する。状態遷移マトリックスを使って、イベントに応じた適切な遷移やエラー処理をチェックするのがポイント。 -
組み合わせテスト
限定的に活用するべき手法で、特定の条件の組み合わせをテストする。適切な範囲設定が必要。
これらの手法を適切に活用し、品質を確保することが大切。
コードベースの単体テスト
日本では単機能テスト(例: 「印刷できる」「URLにジャンプできる」など)も単体テストと呼ばれる場合がある。このような用語の曖昧さは開発に混乱をもたらすため、プロジェクト開始時に「単体テストがコードの確からしさを確認するものか、それとも単機能のテストを指すのか」を明確にすることが重要。
コードベースの単体テストとは
「コードベースの単体テスト」とは、関数の網羅率を計測し、ロジックの正しさを確認するホワイトボックステストのこと。自動車や医療分野ではISO規格に基づき必須とされ、以下を確認する
- 異常動作をしない(例: null ポインタ、0除算)
- 入力値に対して期待通りの出力を返す
- すべての分岐が正しく処理される
厳密な品質管理に欠かせない重要なテスト。
命令網羅(C0 カバレッジ)
命令網羅(C0網羅)は、すべての命令を少なくとも1回実行することを基準とするテスト手法。しかし、境界値やすべての分岐が正しく処理されるかは確認できないため、不十分とされる。
上記では、命令網羅だけでは「すべての分岐条件を満たすテスト」が不足する。このため、分岐網羅のようなより厳密な手法を用いることが推奨される。命令網羅に固執せず、効果的なテストを目指そう。
分岐網羅(C1 カバレッジ)
分岐網羅(C1網羅)は、C0網羅の課題を解決するテスト手法で、単体テストに適している。各判定条件がTRUEとFALSEの結果をそれぞれ少なくとも1回ずつ満たすようにテストケースを作成する。
よくある単体テストの間違いーコードベースの単体テスト
単体テストでよくある間違いは、網羅性だけを重視し、期待される動作や関数の責務を十分に確認しないこと。分岐網羅などの基準を満たすことは重要ですが、目的は単体レベルでのバグを取り除くことにある。適切な入力パターンを網羅し、期待処理を検証することで、後工程でのバグを大幅に削減でき、品質向上に繋がる。
知っているようで知らないコードベースの単体テストの書き方
単体テストの効果的な方法:テスト駆動開発(TDD)
- 赤(Red): 失敗するテストを書く
- 緑(Green): テストを通す最小限の実装を行う
- リファクタリング: コードを整理・改善
これを繰り返すことで、バグの早期発見や安心してリファクタリングできる環境を構築できる。単体テストは品質向上の鍵。
網羅率ーコードベースの単体テストの成否を計測する
コード網羅率は、単体テストの効果を測る重要な指標。
- 目標: 一般ソフトウェアは80%、重要システムは100%。
- Google基準: 60%許容、75%推奨、90%模範。
- 注意点: 高網羅率≠高品質、頻繁に変わるコードを優先。
- 実践: レガシーコードは徐々に改善し網羅率を向上。
効率的なテストでバグ削減と品質向上を目指そう。
単体テストの効率化ー楽勝単体テスト
品質コンサルタントとして現場に入ると、開発者が単体テストを実施していると答えても、実際にはテストコードが管理されていないことが多い。テストコードがデバッガーでの確認に留まることもあり、網羅率が確保されていないケースもある。すべてのコードに対する網羅率を測るのは現実的でないため、重要部分(約20%)に絞って計測し、効率的なテストを行う方法を良い。
コードの複雑度
「複雑度」とは、ソフトウェアの構造の複雑さを測る指標。一般的に、ifやswitch文が多ければ複雑度が高いとされ、逆にそれらが少なければ複雑度が低いと認識される。複雑度が高いとメンテナンス性が低く、逆に低いとメンテナンスがしやすいとされる。それだけ単体テストの数が多くなるため、効率的なテストのためには複雑度を下げることが重要。
どこを単体テストすればよいか?──単体テストやってる暇ありませんという人のために
重要なポイントは、バグが発生しやすい部分(変更頻度の高いファイル)にテストを絞り、無駄な作業を減らすこと。筆者は、ファイルの行数や複雑度も考慮したテスト優先度を設定し、コストを削減しつつ品質を向上させる方法を紹介している。
機能単位の単体テスト
本章では、コードベースでの単体テストに加え、Webアプリなど高品質を要求されないソフトウェアや、さらにUIから機能単位での単体テストが必要な場合のテストについて説明する。特に複雑な機能のテストに焦点を当て、コード単体テストだけではカバーしきれない部分を補うためのテスト戦略を示すことが目的。
開発者がやるべき単機能のテスト
コードベースの単体テストに加え、UIからの機能単位テストについて。重要な手法は「境界値テスト」と「組み合わせテスト」で、例えば年齢やデータ件数の境界値を確認し、複雑なデータを使ったソートのテストを行う。組み合わせテストでは、少数のデータで動作確認を行い、無限に増える組み合わせは適切に絞り込む。開発者はテスト担当者として意識を持ち、バグを早期発見するためのコードレビューや自動化テストを推奨している。
ブラックボックス・ホワイトボックス
ブラックボックステストとホワイトボックステストの選択は、製品の品質とリソースに応じて判断するべき。ブラックボックステストで十分にバグを発見できる場合、コストのかかるホワイトボックステストは不要。しかし、ホワイトボックステストはバグを発見する範囲が広く、特に重要なシステム(例: 車のエンジン制御など)では必須となる。一方で、軽微な影響のあるシステム(例: スマホアプリやカーナビ)では、ブラックボックステストのみで問題ないことが多い。
リファクタリング
リファクタリングの目的は、コード品質を向上させること。
主に2つのアプローチがある。
-
コードの本質論(Martin Fowler推奨): コードを理解しやすく、メンテナンスしやすくするためにリファクタリングを行う。
-
XPのプラクティス(Kent Beck推奨): コーディングリズムを整えるために、コードを少しずつ改善する。
リファクタリングなしでは、アジャイル開発やソフトウェア品質を確保することは難しいとされている。
やはり複雑です、そのコード! 書けません、単体テスト
複雑なコードに対してリファクタリングを行う際、いきなり変更を加えるのではなく、まずは単体テストを作成することが重要。単体テストを書く際、コードの複雑さに手間がかかる場合があるが、以下のリファクタリング手順を踏むことで、コードを改善できる。
- 複雑度を下げる: 複雑なロジックを簡潔に。
- 出口を1つにする: 関数の出口を1つにして、処理を一貫させる。
- MVCを分離する: モデル、ビュー、コントローラーを分ける。
- ファイルのコードを短くする: 長いコードは分割して管理しやすくする。
これらのステップで、複雑なコードをテストしやすく、保守しやすくすることができる。
ファイルのコードのリファクタリング
ファイルのコードが長くなるのは、以下の3つの理由
- 設計不足: コードを適当にファイルに入れてしまう。
- クローンコード: 同じ関数をコピーして使う。
- ビッグクラス: 1つのクラスに多くの責務を持たせる。
これらを避けるために、コードの分割と関数化を行い、責務を明確にすることが重要。
ビッグクラスのリファクタリング
ビッグクラスのリファクタリングは、コードを短くするための重要な手段。ビッグクラスがコードの複雑さを引き起こすため、リファクタリングでクラスを分割することが推奨される。CKメトリックス(特にWMC)は、クラスが大きくなりすぎるとバグの温床になるため、クラスを抽出して小さくすることが重要。これにより、コードがシンプルになり、単体テストが容易になり、メンテナンス性が向上する。
リファクタリング後、コードが小さくなり、テストしやすくなるため、ミスを早期に発見でき、チーム全体でのバグのリスクも減少する。また、コードを修正後、テストが自動で実行される開発スタイルを採用することで、品質を保ちながら効率的に作業を進められる。
複雑度を下げるリファクタリング
複雑度を下げるリファクタリングは、バグを減らし、修正やテストを効率化する。複雑度が高い関数は修正が難しく、バグが発生しやすいため、早期にリファクタリングすることが重要。これにより、コードのメンテナンスがしやすくなり、残業も減らせる。
モデルの複雑度を管理してメンテ性アップより
出口は1つ
コーディングスタンダードを守ることは保守性を高めるが、特に重要なのは関数の出口を1箇所か2箇所に絞ること。2箇所の場合は入り口でのエラーチェックを徹底し、関数内での途中でのreturn
を避けるべき。このスタイルにより、単体テストがシンプルになり、関数の責務に関するテストも容易になる。
MVC分離
MVC(Model-View-Controller)アーキテクチャの分離が担保されていないソフトウェアプロジェクトは非常に困難。途中でMVCを分離するのは手間がかかり、全体の構造に大きな変更が必要となる。特に、日本の多くの企業では、基幹ソフトウェアなどでMVCの分離ができていないことが多い。MVCアーキテクチャを最初から設計段階で考え、実行することが重要。これにより、テストが容易になり、特にView部分を小さくすることで、GUIテストを最小限に抑え、単体テストをより効率的に行うことができる。
コードレビュー
コードレビューとは
コードレビューは、他人のコードを指摘することよりも、書いた本人が気づくことに重点を置くべき。指摘よりも「なぜこうなっているのか?」という問いかけの形式が重要で、これが成長を促進する。また、コードレビューはテストよりも効率的なバグ発見手法であり、適切なレビュープロセスは品質向上に効果的。
最近では、CircleCIやGitHubなどを活用し、効率的なレビューが可能となっている。特に、機械が検出できるバグをレビュー前に処理し、人は最小限のチェックを行う仕組みが推奨されている。単体テストが失敗している場合は、レビューの意味がないため、テスト結果を先に確認することが重要。
また、バグを未然に防ぐことよりも、バグが発生した際にすぐに発見できる仕組みを整えることが重要。
ペアプログラミング
ペアプログラミングは、効率と品質向上に寄与する技術で、特に複雑なシステムや初級者の学習に効果的。2人が1つのPCで作業し、コーディング規約を守ることで無駄な議論を減らし、理解を深めながら進める。研究では、品質向上やテストパス率の向上が確認されており、特に難しい部分には有効。ただし、簡単な作業では必ずしも効率的ではないため、適切に活用することが重要。
結合テスト
結合テストのパターン
統合テストのパターンは3つある
- 単体テスト後、探索的テストのみで統合テスト・システムテストなし
- 単体テスト、統合テスト、システムテストを実施(品質重視)
- 単体テストを省略し、統合テスト・システムテストのみ
統合テスト重視のアプローチでは、ソフトウェアアーキテクチャを見直し、APIテストを徹底的に行うことで、品質の安定性やテスト速度の向上が得られる。
APIテストとAPIバグ密度の考え方
統合テスト(APIテスト)を行う際には、以下のアプローチが有効。
-
境界値テスト: APIの入力パラメータに対して、境界値テストを実施します。例えば、整数の範囲や定数の選択肢に対してテストケースを作成します。
-
状態遷移テスト: 状態遷移を網羅できるようにテストを設計します。これにより、より広範囲なテストが可能となり、テストの質が向上します。
-
API網羅率: コード網羅率を計測する代わりに、テストケースがどれだけ境界値をカバーしているかを計測する。例えば、4つのパラメータごとにテストケースを設定し、すべての組み合わせを網羅する。
実務では、膨大なシステムテストを避け、APIテストでの組み合わせテストが有効。テストは高速で、少しの工夫で効率的に実施できる。
カオスエンジニアリング
カオスエンジニアリングは、システム全体の安定性を確保するために、システムの各部分(プロセス、CPU、仮想マシンなど)を意図的に停止させたり、負荷をかけたりして、システムが障害に強い設計であるかをテストする手法。AWSなどのクラウドサービスでは、簡単に実行できるツールも提供されている。
この手法は、ミューテーションテスト(ソースコードレベルでの壊しテスト)と似ており、システム全体に異常を引き起こし、その振る舞いを観察することで、より現実的なテストが行える。例えば、ネットワーク速度を落としたり、CPUに負荷をかけたりしてシステムの耐障害性を確認する。
カオスエンジニアリングは、単に障害を予測して防ぐのではなく、実際に異常を引き起こしてシステムの動作を確かめる方法。これにより、システムの信頼性を高め、特にクラウドやオートスケーリング環境などの複雑なシステムでは重要なテスト手法となる。
システムテストの自動化
最悪のシステムテスト
キャプチャー・リプレイによる自動化は、ユーザー操作を記録・再生する方法だが、UI変更があるとメンテナンスコストが膨大になり、スクリプトが使い物にならなくなることが多い。最初はコストが低いと感じるかもしれないが、スクリプトのボリュームが増えると維持が困難になる。自動化の最も重要な要素はメンテナンス性であり、キャプチャー・リプレイはその点で最も効率が悪い方法と言える。
キーワード駆動型自動テスト
キーワード駆動型のテスト手法は、アクション(キーワード)とデータを明示的に分けて使うことで、メンテナンス性を向上させる。例えば、UIにおける「ログイン」などのアクションは、複数箇所で繰り返し使われる場合でも、1つのドライバースクリプトでまとめることができる。これにより、UI変更があった場合でも、変更箇所は1箇所で済み、キャプチャー・リプレイ型のように何百箇所を変更する必要がなくなる。
探索的テスト
探索的テストは、スキルのあるテスト担当者がテストケースを書かずに、テストの学習と実行を高速で行う手法。この方法では、テスト設計、実行、結果報告を迅速に行い、テストの効率性を大幅に向上させる。特に、コード網羅率が80%以上で、アーキテクチャがMVCで分離されており、要求仕様がレビューされていれば、探索的テストだけでシステムテストを終わらせることが可能。
探索的テストを導入することで、膨大なテストケースを書く必要もなく、外注にテストを頼むこともなくなり、コストを大幅に削減できる。重要なのは、競合状態(Race Condition)の問題を事前に対策し、テスト対象のコードやシステムが十分に高品質であること。Race Conditionなどの複雑な問題に関しては、静的解析ツールやレビューを通じて事前に発見・対策をすることが求められる。
まとめーテスト全体のデザイン
単体テストは早期に多くのバグを見つけるために重要であり、システムテストで見つけようとすると致命的なバグが残るリスクがある。単体テストはスピードが重要で、チェックイン後にすぐに実行し、小さいビルドと短いサイクルで繰り返すべき。これにより、テストスピードが向上し、バグの早期発見が可能になる。
アジャイル・シフトレフトのメトリックス
シフトレフトとアジャイル開発では、従来の品質メトリックスを見直し、ゼロベースで考えることが求められる。従来のウォーターフォール型の品質メトリックスは、ライフサイクルを通じて品質を評価するものであり、アジャイルではコーディングと同時に品質保証を行うため、これまでのメトリックスは役に立たない。
アジャイルの品質保証は、データ(メトリックス)で判断すべきであり、定量的な品質ゴールを設定し、そのゴールに向けたテストを実施することが重要。代表的なアジャイル向け品質メトリックスには、コード網羅率(C1)、ミューテーションテスト、CKメトリックス、Hotspot、信頼度成長曲線などがある。これらを組織のアジャイル形態に合わせて選定し、ウォーターフォール時代の研究を再利用することも有効。
ミューテーションテスト
ミューテーションテストは、単体テストの有効性を評価するための非常に強力な手法。ここで取り上げられているように、単体テストが網羅率を達成していても、期待値チェックが不十分であったり、特定のケースを見逃している場合があるため、ミューテーションテストはこれらの問題を指摘し、改善の手助けとなる。
ミューテーションテストの流れ
- 単体テストを用意: まず、すべての単体テストが通る状態にしておく。
- ミュータントを生成: ミューテーションツールを使い、意図的にコードを変更(ミュータントを仕込む)する。たとえば、条件分岐の演算子を変えたり、返り値を変更したりする。
- テスト実行: 変更後のコードに対して単体テストを実行する。ここで、もしテストが失敗すれば、ミュータントを「殺した」と見なされる。逆に、テストが通れば、ミュータントは「生き残った」ことになる。
ミューテーションテストのメリット
- テストの質向上: 期待値チェックやカバレッジが不十分なテストを見つけ出すのに役立つ。
- 隠れたバグの発見: バグを意図的に導入し、それに対してテストケースが反応するかを確認するため、見落としていたバグを発見できる。
- テストケースの見直し: すべてのテストが100%パスするわけではないため、テストケースの改善が促進される。
ミューテーションテストの課題
- 高いコスト: 実行に時間がかかり、特に大規模なコードベースでは負担が大きくなる。
- 誤検出: テスト対象外のコード(例えばエラー処理)に対してミュータントを仕込むと、誤検出が発生する可能性があり、レビューが必要になる。
- テストケースの作成難易度: 十分なカバレッジを持ったテストを作成していないと、ミューテーションテストの結果が意味を持たない。テストケースの網羅性が高いことが前提となるため、テストの準備が重要。
ミューテーション網羅率
ミューテーションテストの結果は、単にテストの網羅率だけではなく、「ミュータントをどれだけ殺せたか」を示す。この値が高いほど、テストの有効性が高いと判断できる。しかし、実際にはミューテーションの生成方法やその取り扱いが多様であるため、数値化するのが難しく、適切な解釈が必要。
これらの点を踏まえて、ミューテーションテストは有効な改善手法でありながらも、その導入には一定の労力や開発者のスキルが要求される。しかし、長期的にはソフトウェアの品質向上に大いに貢献するだろう。
ユーザーストーリと信頼性メトリックス
アジャイル開発におけるユーザーストーリとその信頼性メトリックスの関係について、J.D. Musaの提案した「オペレーショナルプロファイル」という信頼性工学の手法がある。オペレーショナルプロファイルは、ユーザーストーリに基づく状態遷移テストを行い、ソフトウェアの信頼性を定量的に測定するための方法。この方法は、信頼性評価に有効であり、品質保証担当者と協力しながら進めることが推奨される。
信頼度成長曲線のメトリックス
信頼度成長曲線を使ったメトリックスの作成方法について。信頼度成長曲線は、ソフトウェアのテストを通じて見つかるバグの数と発見率に基づいて、信頼性(MTBF:平均故障間隔)を予測する手法。特にアジャイル開発では、ユーザーストーリに基づくテスト結果を使って、各イテレーションの信頼性を計測し、リリースの基準を定量的に設定することが可能。最終的には、ユーザーシナリオを実行した際のバグ発生間隔を測定し、出荷判断の参考にする。
アジャイルにおける要求仕様
アジャイル開発では、従来の要求仕様は「ユーザーストーリ」として表現される。ユーザーストーリは、顧客と開発チームの「約束」であり、変更可能。良いユーザーストーリは、理解しやすく、簡潔で、価値を提供するもの。重要な特徴は「独立」「交渉可能」「価値がある」「見積もり可能」「小さい」「テスト可能」で、これにより柔軟で実用的な開発が進められる。
ユーザーストーリの利点
アジャイル開発では、ユーザーストーリを開発者が適切に書き、テスト担当者がイテレーション期間中に確認することが重要。ウォーターフォールモデルでは、要求仕様の欠陥が出荷後に発覚すると大きな損失を招くことがあるが、アジャイルではユーザーストーリをイテレーション終了時にステークホルダーと確認できるため、品質の観点から非常に効果的。この点だけでもアジャイルは採用に値するかもしれない。
最後に
ソフトウェア工学の基本的な考え方は、2000年代初頭にはほぼ完成されたと筆者は考えている。新たな革新は少ないため、以下の3つの技術を組み合わせれば、世界最高水準の開発が可能。
- アジャイルでの高速開発
- CI/CDによる迅速なデリバリ
- Hotspotを利用した単体テスト
Discussion