ソフトウェア工学シリーズの第3回目だよ。
システムテストの意義、目的、および原則
システムテストは、エラーを見つけるためにプログラムを実行するプロセスのことだよ。成功するテストとは、まだ発見されていないエラーを見つけ出したテストのことなんだ。
テストの目的は、最小限の労力と時間で、潜在的なあらゆるエラーや欠陥を見つけ出すこと。ユーザーは、開発の各段階での要件や設計ドキュメント、あるいはプログラムの内部構造に基づいてテスト用例(テストケース)を慎重に設計し、それらを使ってプログラムを動かしてエラーを見つける必要があるよ。
情報システムのテストには、ソフトウェアテスト、ハードウェアテスト、ネットワークテストが含まれるべきだね。ハードウェアやネットワークのテストは具体的な性能指標に基づいて行われるけど、ここで言う「テスト」は主にソフトウェアテストを指しているよ。
システムテストは、システムの品質と信頼性を保証するための重要なステップで、システム分析、システム設計、実施という開発プロセス全体の最終チェックでもあるんだ。テストの概念と目的に基づいて、システムテストを行うときは以下の基本原則に従おう。
- 早めに、そして継続的にテストを行うこと。 テストはアプリの開発が終わってからやるものじゃないんだ。問題の複雑さや開発段階の多様性、メンバー間の調整不足なんかで、どの段階でもエラーは発生しうる。だから、テストは開発の全段階を通して行い、早めにエラーを修正して火種を消しておくのが大事だよ。
- 開発者本人やそのチームがテストを行うのは避けること。 開発者は自分の仕事を否定したくないし、「自分の作ったソフトに間違いはない」と思いがちだからね。それに、自分のプログラミングの思考回路に引きずられてテストの視点が偏ってしまうこともある。客観的で効果的なテストにするために、専門の担当者が行うのがベストだよ。
- テスト計画を立てる時は、入力データだけでなく、期待される出力結果も決めておくこと。 実際の出力と期待値を比べることで、正しく動いているかどうかがわかるからね。
- 妥当で合理的な入力条件だけでなく、不合理なものや無効な入力条件もテストケースに含めること。 人はどうしても「正しく動くはず」のケースばかりテストしがちだけど、異常な事態や予想外のケースにこそ落とし穴があるんだ。
- プログラムが「すべきこと」をしているかだけでなく、「すべきでないこと」をしていないかも確認すること。 余計な動作はサイドエフェクト(副作用)を生んだり、効率を下げたり、時には潜在的な害やエラーを引き起こすことがあるよ。
- テスト計画に厳格に従い、行き当たりばったりのテストは避けること。 計画には内容、スケジュール、担当者、環境、ツール、資料などを盛り込もう。計画通りに進めることで、全体の足並みを揃えることができるよ。
- テスト計画やテストケースは、ソフトウェアドキュメントの一部として大切に保管すること。 後のメンテナンスで役に立つからね。
- テストケースは再利用できるように設計すること。 エラー修正後や機能追加後の再テスト(リポジトリテスト)は繰り返し作業が多いから、以前のケースを使い回したり修正したりできれば効率的だよ。
システムテスト段階のテスト目標は、要件分析段階から導き出されるものなんだ。
伝統的なソフトウェアのテスト戦略
効果的なソフトウェアテストは、実際には「ユニットテスト」「統合テスト」「確認テスト」「システムテスト」の4つのステップで行われるよ。
(一) ユニットテスト
ユニットテスト(単体テスト)はモジュールテストとも呼ばれていて、モジュールのコーディングが終わってコンパイルエラーがなくなったらすぐに始められるよ。モジュール内部の処理ロジックやデータ構造に焦点を当てるんだ。マシンを使う場合は、一般的にホワイトボックステスト法が使われるよ。複数のモジュールを同時にテストすることも可能だね。
(1) ユニットテストのテスト内容
ユニットテストでは、主にモジュールの以下の5つの特徴をチェックするよ。
- モジュールインターフェース。 データが正しく出入りしているかを確認するよ。
- 入力引数と仮引数の個数、型、単位が一致しているか。
- 他のモジュールを呼び出す際の実引数と仮引数が一致しているか。
- 標準関数を使う時の引数の型、数、順序が正しいか。
- グローバル変数の定義と使い方が各モジュールで一致しているか。
- 入力が仮引数だけを変えていないか。
- I/O(入出力)の形式が文と一致しているか。
- ファイルの使用前後で適切にオープン/クローズされているか。
- 局所データ構造。 ここでのミスはよくあることだから、重点的にチェックしよう。
- 変数の宣言が適切か。
- 値が入っていない、または初期化されていない変数を使っていないか。
- 初期値やデフォルト値が正しいか。
- 変数名の間違い(スペルミスなど)はないか。
- 重要な実行パス。 パスのテストは基本中の基本。全部のパスを試すのは無理だから、計算、比較、制御フローのエラーを見つけられるようなテストケースを工夫して作ろう。
- 計算ミス:演算の優先順位、精度不足、型の不一致、アルゴリズムの間違いなど。
- 比較・制御フロー:精度のせいで一致しない、異なる型の比較、論理演算子の間違い、ループの終了条件ミス(1回多い、少ない)、無限ループ、ループ変数の不適切な書き換えなど。
- エラー処理。 良い設計はエラーを予測し、それを処理するパスを持っているものだよ。エラーメッセージが出るだけでなく、その後のロジックが正しいことも大事なんだ。
- 境界条件。 境界条件のテストはユニットテストの仕上げであり、とても重要だよ。ソフトウェアは境界部分でエラーが起きやすいからね。
(2) ユニットテストのプロセス
モジュールは単独では動かないから、呼び出し関係を再現するために2種類の補助モジュールを作る必要があるよ。
- ドライバ (Driver)。 メインプログラムのような役割で、テストデータを受け取ってテスト対象のモジュールに渡し、結果を出力するよ。
- スタブ (Stub)。 テスト対象モジュールが呼び出している下位モジュールの代わりをするもの。入り口のチェックや、呼び出し・戻りの情報を出力したりするよ。

モジュールの内聚度(結合度)を高めると、ユニットテストは楽になるよ。各モジュールが一つの機能に集中していれば、テストケースも減らせるし、エラーも見つけやすくなるんだ。
(二) 統合テスト
統合テスト(結合テスト)は、システム設計書に従って各モジュールを組み合わせて行うテストだよ。個別のモジュールがパスしていても、組み合わせた途端に問題が出ることはよくあるんだ。
やり方は大きく分けて2つ。
- 非増分統合: 全部バラバラにテストしてから、一気にガッチャンコしてテストする。
- 増分統合: 少しずつ組み合わせて、段階的に作ってテストしていく方法。
増分統合にはいくつかの戦略があるよ。
(1) トップダウン統合テスト
構造の上(メインプログラム)から順に、階層を降りながら組み合わせていく方法。深さ優先(Depth-first)か幅優先(Breadth-first)で進めるよ。

- メインモジュールをドライバとして使い、その下のモジュールをスタブに置き換える。
- 実際のモジュールを一つずつスタブと入れ替えていく。
- 入れ替えるたびにテストを行う。
- 回帰テストを行って、新しいエラーが出ていないか確認する。
- 全部終わるまで繰り返す。
ドライバを作る必要はないけど、スタブをたくさん作る必要があるよ。
(2) ボトムアップ統合テスト
一番下のモジュール(原子モジュール)から順に組み合わせていく方法。下から順に作っていくから、スタブを作る必要がないんだ。

- 低層のモジュールを組み合わせて「クラスター」を作る。
- テスト用のドライバを作る。
- クラスターをテストする。
- ドライバを外して、上の階層へ組み合わせていく。
スタブはいらないけど、ドライバを作る必要があるよ。
(3) 回帰テスト (Regression Testing)
新しいモジュールを追加すると、データの流れが変わったり、思わぬところに影響が出たりすることがある。回帰テストは、変更によって「今まで動いていた機能」が壊れていないかを確認するために、テスト済みの項目をもう一度やり直すことだよ。
手動で行うこともあるし、自動化ツール(キャプチャ/リプレイツール)を使うこともあるね。全部やり直すと大変すぎるから、代表的なケースや影響を受けそうな機能に絞って行うのが現実的だよ。
(4) スモークテスト
開発中によく行われる統合テストの一種で、主要な機能がとりあえず動くかどうかをサクッと確認するものだよ。頻繁に評価することで、プロジェクトの状態を把握しやすくするんだ。
テスト方法
ソフトウェアテストには、大きく分けて「静的テスト」と「動的テスト」があるよ。
- 静的テスト。 プログラムを動かさずに、人間がチェックしたりツールで分析したりする方法。
- 人手によるチェック:コードレビューやインスペクションなど。
- コンピュータ支援静的分析:分析ツールを使って、ロジックの欠陥や疑わしい構造を抽出するよ。
- 動的テスト。 実際にプログラムを動かしてエラーを見つける方法。これには「ブラックボックステスト」と「ホワイトボックステスト」があるよ。
テストケースは「入力データ」と「期待される結果」のセットで作るんだ。
(一) ブラックボックステスト
機能テストとも呼ばれるよ。中身(コード)がどうなっているかは気にせず、外側から見た機能や仕様が正しいかをチェックするんだ。
主な手法は以下の通り。
(1) 等価類分割 (等価分割法)
入力データを、同じ結果が得られるはずのグループ(等価類)に分けて、各グループから代表的なデータを一つ選んでテストする方法。効率よくテストできるよ。有効なデータ(有効等価類)と無効なデータ(無効等価類)の両方を考えるのがコツ。
(2) 境界値分析
エラーは入力の端っこ(境界)で起きやすいから、そこを重点的に狙う方法。等価分割と組み合わせて使うことが多いよ。
(3) エラー推測
経験や勘を頼りに、「ここらへんが怪しいな」という部分を狙い撃ちする方法。過去のミスなどをリストアップしてテストケースを作るよ。
(4) 因果グラフ
仕様書から「原因(入力)」と「結果(出力)」の関係を整理して、判定表(デシジョンテーブル)に落とし込んでテストケースを作る方法だよ。
(二) ホワイトボックステスト
構造テストとも呼ばれるよ。プログラムの内部ロジックやパスが設計通りになっているかをチェックするんだ。
主な原則は:
- 全ての独立したパスを少なくとも1回は実行する。
- 全ての論理判定で「真」と「偽」の両方を少なくとも1回は実行する。
- ループの境界や一般的な条件をチェックする。
- 内部データ構造の有効性を確認する。
(1) 論理網羅 (ロジックカバレッジ)
どの程度ロジックをカバーしたかを示す指標がいくつかあるよ。
- 命令網羅 (Statement Coverage)。 全ての文を1回は通る。一番弱い。
- 判定網羅 (Decision/Branch Coverage)。 各判定の「真」「偽」を少なくとも1回は通る。
- 条件網羅 (Condition Coverage)。 判定文の中にある個々の条件式が「真」「偽」の両方を取るようにする。
- 判定/条件網羅。 判定そのものの「真/偽」と、中の個々の条件の「真/偽」の両方を満たす。
- 条件組合せ網羅。 判定内の条件のあらゆる組み合わせをテストする。
- 経路網羅 (Path Coverage)。 あり得る全てのパスを通る。一番強い。
(2) ループ網羅
ループ内の各条件が検証されるようにテストするよ。
(3) 基本経路テスト
プログラムのフローグラフを分析して、基本的な実行パスの集合を導き出し、そこからテストケースを作る方法だよ。
デバッグ
デバッグはテストの後にやる作業で、見つかったエラーの原因と場所を特定して、修正すること。
よく使われる手法はこんな感じ。
(1) 試行錯誤法 (Brute Force)
エラーの症状から場所を推測して、出力文(printデバッグ)を入れたりメモリの中身を見たりして、一歩ずつ原因に迫る方法。
(2) バックトラッキング法 (回溯法)
エラーが出た場所からコードを逆に辿っていって、どこでおかしくなったかを見つける方法。
(3) 二分探索法 (対分查找法)
正しい値がわかっている地点にチェックポイントを置いて、エラーの範囲を半分ずつ絞り込んでいく方法。
(4) 帰納法 (帰納法)
起きた問題のデータを集めて、それらの関係性を分析し、「これが原因じゃないか?」という仮説を立てて証明していく方法。
(5) 演繹法 (演繹法)
考えられる原因を全部リストアップして、矛盾するものやあり得ないものを消去法で消していき、残った可能性の高い原因を検証する方法だよ。