ソフトウェアテの基本概念
現代のテクノロジー発展に伴い、ソフトウェアの規模はますます拡大し、多くの業界の中核業務を支えています。これらの大規模ソフトウェアを実行する際には、プログラムの正確性と信頼性を維持することが極めて重要です。エラーによる不必要な損失を防ぐため、ソフトウェアテスト(Software Testing)はソフトウェア品質管理において最も直接的で実践的な手段となっています。
ソフトウェア欠陥の定義
欠陥(Fault/Defect)は、プログラム内に現れる誤差の表れ(静的)です。
- 誤実装欠陥(Wrong):仕様書が正しく実装されていない場合
- 欠如欠陥(Missing):規定または期待される要件が製品に反映されていない場合
- 過剰欠陥(Extra):仕様書に規定されていない要件が実装されている場合
エラー(Error)とは、プログラムの欠陥が実行された際に発生する予期せぬプログラム内部状態(動的)です。
障害(Failure)とは、エラーが外部に伝播した際に生じる結果です。
以下の条件を満たす場合、それはソフトウェア欠陥と見なされます:
- ソフトウェアが製品仕様書に要求された機能を実装していない
- ソフトウェアが製品仕様書で発生しないと明記されているエラーを表示する
- ソフトウェアが製品仕様書に記載されていない機能を実装している
- ソフトウェアが製品仕様書で明示的に記載されていないが実装すべき目標を達成していない
- ソフトウェアが理解しにくく、使用が困難、または実行が遅い場合
欠陥発生の原因
ソフトウェア要件仕様書(RS-Requirement Specification)
- 作成されていない
- 包括的、正確、詳細でない
- 頻繁に変更される
ソフトウェア設計(Architecture)
- 説明が不明確
- フレームワークの結合が緊密で一貫性がない
- ソフトウェアフレームワークが頻繁に変化し、不安定
コードエラー
- 開発者のスキルと技術レベル
- 上流の分析と設計による問題
ソフトウェアのエラーと欠陥は、開発プロセスの後半になるほど問題が大きくなり、損失も増加する可能性があります。
ソフトウェアテストのプロセス
ソフトウェアテストは4つのステップで構成されます:テスト計画の策定、テストケースとテストスクリプトの設計、テストケースの実行、テスト結果の評価。
ソフトウェアテストの中核はテストケースにあります。テストケース = 入力 + 期待出力
入力:入力データと前提条件(テストケース実行前に存在する環境)
期待出力:出力結果と事後条件(テストケース実行後に生じる環境)
テストの最終段階では、実際の実行結果が期待結果と一致するかを確認します。一致すればエラーはなく、テストは成功です。
テスト手法の分類
ブラックボックステスト:機能テスト、仕様ベースのテスト
ホワイトボックステスト:構造テスト、透明ボックステスト、コードベースのテスト
これらに加え、グレーボックステストもあります。これは通常、ソースコードに直接アクセスできない場合に、ソフトウェア成果物や逆コンパイルなどの手段を通じてコードの構造情報を部分的に取得し、テストを行う手法です。
ブラックボックステストの詳細
ブラックボックステストでは、プログラムを入力定義域から出力値域へのマッピングを行う関数と見なし、システムを「ブラックボックス」として扱います。テストケース設計の唯一の根拠はソフトウェアの仕様説明であり、ソフトウェアの具体的な実装とは無関係です。
ブラックボックステストの手法
境界値テスト:各種境界状況に対してテストケースを設計し、より多くのエラーを発見することを目指します。プログラムの設計とコーディングでは、仕様説明の入力境界や出力境界が十分に注意されないことが多いためです。
同値クラステスト:入力ドメインのサブセット内の各データがプログラムのエラーを発見するために等価であるという考えに基づきます。同値クラスの集合を分割することで、完全性を保証し、冗長性を排除します。
決定表ベースのテスト:複雑な論理関係を分析および表現するのに適しています。詳細設計およびテスト段階の両方で使用できます。
因果グラフテスト:入力条件の様々な組み合わせと、入力条件間の相互制約関係を考慮します。等価クラス分割や境界値分析が主に入力条件に焦点を当てるのに対し、因果グラフは複数の入力条件の組み合わせによるエラーを発見できます。
直交表ベースのテスト:直交実験計画法に由来し、多数のデータから適量で代表的な点を選択し、テストを合理的に配置する科学的な実験計画法です。
ホワイトボックステストの詳細
ホワイトボックステストでは、プログラムの実装が既知であり、システムを「透明なボックス」と見なします。プログラム内部の論理構造および関連情報を利用して、テストケースを設計または選択します。テストケース設計の唯一の根拠はプログラムの実装です。
ホワイトボックステストの手法
論理カバレッジ:プログラムの内部論理構造に基づく動的ホワイトボックステスト手法です。
- 文カバレッジ(SC):テストプロセスにおいて、テスト担当者が十分な数のテストケースを選択し、テスト対象プログラムの各文が少なくとも1回は実行されるようにします。
- 判定カバレッジ(DC):プログラムの各判定の真の分岐が少なくとも1回、偽の分岐も少なくとも1回経験されるようにします。
- 条件カバレッジ(CC):各判定の各サブ条件の可能な値が少なくとも1回実行されるようにします。
- 条件/判定カバレッジ(C/DC):各判定の全分岐が少なくとも1回実行され、各判定の各サブ条件の可能な値も少なくとも1回実行されるようにします。
- パスカバレッジ:プログラム内のすべての可能なパスをカバーします。
基本パステスト:プログラムフローチャートを制御フローグラフに変換し、制御構造の環路複雑性を分析して基本パスの独立セットを見出し、最終的にテストケースを導出します。
データフローテスト:プログラム内の変数の定義と使用位置に基づいてプログラムのテストパスを選択する手法です。主にコードの最適化に使用されます。
ソフトウェアテストのレベル
単体テスト:詳細設計仕様書とソースコードリストに基づき、プログラムの最小単位にエラーがないかを検証します。
結合テスト:単体テストを完了したモジュールを設計要件に従ってサブシステムやシステムに統合し、モジュール間のインターフェースと相互作用のエラーを発見します。
システムテスト:確認テストを完了したソフトウェアとシステムの他の要素を組み合わせ、実際の運用環境でコンピュータシステムの一連の統合テストと確認テストを行い、システム要件定義に適合しない箇所を発見します。
ソフトウェアテストのプロセスとツール
ソフトウェアテストツールは、テスト計画と管理、ソースコード管理、自動テストケース生成、標準テストケースパッケージ、メモリリークテスト、テストフレームワーク、キャプチャ、再生と比較、負荷テストのシミュレーションなどの機能を提供します。
ツールの利点と欠点
利点:
- テストプロセスとデータの標準化、規範化
- プロジェクト計画、開発計画との統合
- テストケース、欠陥報告、欠陥分析とテスト計画の統合
- テストドキュメント管理
- 欠陥追跡と管理、テスト評価
- テストスクリプトとテストケースの再利用、再編集が可能
- テストデータとテストプロセス/スクリプトの分離
- 回帰テスト、負荷テスト、ストレステストに適している
- プログラム内部情報の観察が可能
欠点:
- 費用リスク
- 統合問題
- 銀の弾丸リスク
- テストスイート
- ローカライゼーション問題
ソフトウェア品質保証
ソフトウェア品質とは、ソフトウェアが以下の要件に適合する度合いを指します:
- 明確に定義された機能と性能要件
- 明確に規定された開発基準とガイドライン
- 暗黙的に要求されるその他の特性
ソフトウェア品質保証(SQA)の目標は、リスクを回避し、ソフトウェア開発プロセスと開発結果の期待との不一致を減少・修正することです。ソフトウェア開発の各段階でプロセス制御を行い、欠陥に焦点を当てます。
事例研究:ソフトウェア欠陥
Equifaxデータ漏洩事件
2017年9月、米国でEquifaxデータ漏洩事件が発生しました。この事件の結果、会社は重大な財務的損失と評判の低下を被り、影響を受けた顧客や規制当局から法的措置が取られました。また、数百万人の個人情報が漏洩し、潜在的な身元盗用やその他のサイバー犯罪につながる可能性がありました。この事件は、定期的なセキュリティ更新とソフトウェアの徹底的なテストの重要性を強調しました。
原因と問題分析
- パッチ管理プロセス:Equifaxはソフトウェアやアプリケーションの多くの脆弱性に対して、ソフトウェアパッチのみを使用して脆弱性に対処し、感染しやすいシステムへのアクセスを制限していました。データ漏洩の直接的原因となったApache Struts脆弱性に対して、Equifaxは消費者データを保護するために簡単で低コストのパッチを効果的に使用していませんでした。
- エンドポイントとメールセキュリティの不十分な監視:Equifaxは厳格なエンドポイントとメールセキュリティ対策を採用していませんでした。
- 機密情報の漏洩:Equifaxは機密情報を保護するための有効な措置を講じていませんでした。ハッカーがApache Strutsの脆弱性を利用してEquifaxシステムのアクセス権を取得すると、顧客情報データベースが発見されました。
- ネットワークセグメンテーションの脆弱性:Equifaxには、インターネットに面した安全でないシステムから、より価値のあるデータを含むバックエンドシステムへのハッカーの移動を防ぐセキュリティ対策がありませんでした。
- 不適切な証明書認可:厳格なセキュリティ基準では、Equifaxは重要なデータシステムへのアクセスを制限し、内部攻撃から会社を守るはずでした。しかし、ハッカーがシステムに侵入すると、消費者資格情報(ユーザー名とパスワード)を取得し、これらを使用して大量の機密情報にアクセスしました。
- ログ記録:Equifaxは、ハッカーをシステムから排除し、データ漏洩の規模と範囲を制限できる堅牢なログ記録技術を採用していませんでした。
ブラックボックステスト手法の事例
課題
ある報告処理システムでは、ユーザーが処理する報告書の日付を入力する必要があります。日付は2001年1月から2013年12月の範囲に制限されており、システムはこの期間内の報告書のみを処理できます。範囲外の日付の場合は、入力エラーメッセージが表示されます。システム日付は、年、月の6桁の数字文字で構成され、最初の4桁が年、最後の2桁が月を表します。同値クラス分割法を使用して、プログラムの日付チェック機能をテストするためのテストケースを設計してください。
解決策
まず、仕様を分析し、同値クラス分割の原則に基づいて同値クラスを特定します。入力日付の正しい範囲は2001年1月から2013年12月です。入力日付を以下の同値クラスに分割できます:
- 入力日付が2001年1月から2013年12月の間(有効)
- 入力日付が2001年1月より前(無効)
- 入力日付が2013年12月より後(無効)
- 6文字でない入力(無効)
- 数字以外の文字を含む入力(無効)
- 月が1より小さい入力(無効)
- 月が12より大きい入力(無効)
有効同値クラスと無効同値クラスの両方をカバーするテストケースを設計します。
import java.util.Scanner;
public class DateValidator {
public static void main(String[] args) {
Scanner inputDevice = new Scanner(System.in);
System.out.println("日付を入力してください(YYYYMM):");
String dateInput = inputDevice.nextLine();
if (dateInput.length() != 6) {
System.out.println("日付入力エラー:文字列の長さが6ではありません。");
return;
}
try {
int yearValue = Integer.parseInt(dateInput.substring(0, 4));
int monthValue = Integer.parseInt(dateInput.substring(4, 6));
if (yearValue < 2001 || yearValue > 2013 || monthValue < 1 || monthValue > 12) {
System.out.println("日付入力エラー:年または月が有効範囲外です。");
return;
}
} catch (NumberFormatException e) {
System.out.println("日付入力エラー:文字列に数字以外の文字が含まれています。");
return;
}
System.out.println("入力された日付は有効です。");
}
}
ホワイトボックステスト手法の事例
課題
挿入ソートアルゴリズムを基本パスカバレッジでテストします。
解決策
まず、制御フローグラフを描画し、環路複雑性を計算します。McCabe測定法によれば、環路複雑性は制御フローグラフの領域数に等しいです。
public class InsertionSort {
public void sortArray(int[] elements, int size) {
if (size <= 1) {
return;
}
for (int i = 1; i < size; i++) {
int keyValue = elements[i];
int position = i - 1;
while (position >= 0 && elements[position] > keyValue) {
elements[position + 1] = elements[position];
position--;
}
elements[position + 1] = keyValue;
}
}
}
環路複雑性V(G)を計算します:
- 方法1:フローグラフの領域数からV(G) = 4
- 方法2:V(G) = E - N + 2 = 11 – 9 + 2 = 4(Eは辺の数、Nはノードの数)
- 方法3:V(G) = P + 1 = 3 + 1 = 4(Pは判定条件の数)
線形独立な基本パスセットを特定し、各パスの実行を保証するテストケースを生成します。
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
public class InsertionSortTest {
@Test
public void testEmptyArray() {
InsertionSort sorter = new InsertionSort();
int[] emptyArray = {};
sorter.sortArray(emptyArray, emptyArray.length);
assertArrayEquals(new int[]{}, emptyArray);
}
@Test
public void testSingleElement() {
InsertionSort sorter = new InsertionSort();
int[] singleElement = {42};
sorter.sortArray(singleElement, singleElement.length);
assertArrayEquals(new int[]{42}, singleElement);
}
@Test
public void testAlreadySorted() {
InsertionSort sorter = new InsertionSort();
int[] sortedArray = {10, 20, 30, 40};
sorter.sortArray(sortedArray, sortedArray.length);
assertArrayEquals(new int[]{10, 20, 30, 40}, sortedArray);
}
@Test
public void testUnsortedArray() {
InsertionSort sorter = new InsertionSort();
int[] unsortedArray = {30, 10, 40, 20};
sorter.sortArray(unsortedArray, unsortedArray.length);
assertArrayEquals(new int[]{10, 20, 30, 40}, unsortedArray);
}
}
テストツールの事例
JUnitフレームワーク
JUnitはJava言語のためのオープンソースの単体テストフレームワークで、Java専用に設計されており、最も広く使用されています。JUnitは事実上の単体テストの標準フレームワークであり、すべてのJava開発者がJUnitを使用して単体テストを作成すべきです。JUnitを使用して単体テストを作成する利点は、テストコードを非常に簡単に整理し、いつでも実行でき、成功したテストと失敗したテストを表示し、テストレポートを生成できることです。
JUnitの使用例
import org.junit.Test;
import static org.junit.Assert.*;
public class ArithmeticOperationsTest {
private ArithmeticOperations calculator;
@Test
public void testAddition() {
calculator = new ArithmeticOperations();
assertEquals("正の数の加算に問題があります", 5, calculator.add(2, 3));
assertEquals("負の数の加算に問題があります", -5, calculator.add(-2, -3));
assertEquals("正負の数の加算に問題があります", 0, calculator.add(2, -2));
}
@Test
public void testSubtraction() {
calculator = new ArithmeticOperations();
assertEquals("正の数の減算に問題があります", 2, calculator.subtract(5, 3));
assertEquals("負の数の減算に問題があります", 1, calculator.subtract(-2, -3));
assertEquals("正負の数の減算に問題があります", 5, calculator.subtract(3, -2));
}
@Test
public void testMultiplication() {
calculator = new ArithmeticOperations();
assertEquals("正の数の乗算に問題があります", 6, calculator.multiply(2, 3));
assertEquals("負の数の乗算に問題があります", 6, calculator.multiply(-2, -3));
assertEquals("正負の数の乗算に問題があります", -6, calculator.multiply(2, -3));
}
@Test
public void testDivision() {
calculator = new ArithmeticOperations();
assertEquals("正の数の除算に問題があります", 2, calculator.divide(6, 3));
assertEquals("負の数の除算に問題があります", 2, calculator.divide(-6, -3));
assertEquals("正負の数の除算に問題があります", -2, calculator.divide(6, -3));
}
@Test(expected = ArithmeticException.class)
public void testDivisionByZero() {
calculator = new ArithmeticOperations();
calculator.divide(5, 0);
}
}
単体テストを作成する際の注意点:
- テストコード自体が非常にシンプルで、一目で理解できる必要があります。テストコードのテストを作成する必要はありません。
- 各単体テストは互いに独立しており、実行順序に依存しないようにします。
- テスト時には、一般的なテストケースだけでなく、入力が0、null、空文字列""などの境界条件にも特に注意を払います。