
0.はじめに
テストとは、責任追及から逃れるための証跡 である。
普段から、質の良い証跡・アリバイ作り を心がけましょう。
本記事では、やるべきことはやった を論理的・機械的に示せる、テストケース算出手法 を紹介します。
1. テストケース設計の重要さ
ソフトウェア開発におけるテストの手段は、年々高度になっています。
特に、単体テストやE2Eテストといった工程では、フレームワークや自動化ツールの進化によりテストを「どう実行するか」の部分が格段に効率化されました。
しかしその一方で、「何をテストするか」という根本的な問い、すなわちテストケースの設計は、いまだに人間の経験と勘に依存しているケースが多いです。
これは属人的であるがゆえに、漏れや責任問題につながりやすい領域でもあります。
脳死である程度のカバレッジを担保できる設計手法があるのであれば、それを使わない手はありません。
本記事では、この「人間の思考に依存する領域」に対し、論理的・機械的なアプローチで精度を高める方法を紹介します。
完璧に使いこなして、「やるべきテストはやった」と自信を持って上司・顧客を倒せるようになりましょう。👊
本記事はテスト設計技法の紹介を行うものであり、
これによって発生した一切の損害に対して責任を負いません。
2. 前提 テスト手法の大分類
テスト設計の手法は大きく分けて、以下の2つの視点に整理できます。
ブラックボックステスト
要件や仕様書だけをよりどころにしたテスト手法。
システムの内部的な実装・構造の知識を必要とせず、想定されている入出力の挙動を確認する。
活用しやすい反面、すべての分岐を検証することが難しいため精度に欠ける。
ホワイトボックステスト
システムの内部構造を考慮したうえで行われるテスト手法。
開発者がソースコードを直接参照して行うため、命令・分岐・条件を網羅することができる。
精度は高いが、テストケースが膨大になることがほとんどであり、すべてを実施することは現実的でない。
→ 場合によっては、テスト担当者による、適切な取捨選択 が求められる。
テストケースを効率的かつ漏れなく設計するためには、状況や目的に応じて適切なアプローチを取ることが重要です。
ここからは、ブラックボックス・ホワイトボックスの2視点で、テストケースの算出手法を紹介していきます。
3.ブラックボックステストの手法
3.1 同値クラステスト(同値分割法)
入力値を「同じ結果になるグループ(同値クラス)」に分けて、その代表値だけをテストすることで、工数を減らしつつテスト網羅性を保つ手法です。
この グループ という考え方は、様々な設計手法の根底的な概念です。
例として、「1以上100以下の整数」を受け付ける入力フォームを考えます。
この場合は以下のような分類になります。
- 有効な値(有効同値クラス):
1〜100
- 無効な値(無効同値クラス):
0以下、101以上、非整数、null...
// テスト対象
function isValidScore(int $score): bool {
return 1 $score && $score 100;
}
// テストケース
assert(isValidScore(50) === true); // 有効等価クラス
assert(isValidScore(0) === false); // 無効等価クラス
assert(isValidScore(150) === false); // 無効等価クラス
本来、有効値が100ケース、無効値が無限ケースあることを考えると、
各グループの代表値3ケースのみになったため非常にテストのコストが減りました。
しかし、これではコード内の条件式に不備があった場合や、想定してない無効値が入力された場合をカバーできません。
3.2 境界値テスト
同値分割の考えを拡張したものが境界値分析です。
値の「境界」にバグが潜みやすいという考えのもと、各同値クラスの境目にある値をテストケースに追加します。
例として、下記のような実装を考えてみます。
// 誤り
function isValidScore(int $score): bool {
return 1 $score && $score 100;
}
// 正しい
function isValidScore(int $score): bool {
return 1 $score && $score 100;
}
誤った実装では、1
を無効値として扱ってしまっていますね。
同値分割でテストをした場合、運良く1
を有効クラスの代表値に選ばない限りはこのバグを見逃してしまいます。
このような、条件の境目(境界値)のバグ検出に有効な手法が境界値分析です。
境界値テストでは、同値分割のケースに境界値を加えます。
(今回の境界値は 0
,1
,100
,101
)
// テストケース
assert(isValidScore(50) === true); // 有効等価クラス
assert(isValidScore(0) === false); // 無効等価クラス
assert(isValidScore(150) === false); // 無効等価クラス
// ------------追加-------------
assert(isValidScore(0) === false); // 無効境界
assert(isValidScore(1) === true); // 有効境界
assert(isValidScore(100) === true); // 有効境界
assert(isValidScore(101) === false); // 無効境界
// -----------------------------
範囲条件を含む実装については、必ず境界値をテストケースを含みましょう。
Q&A
Q. この場合、無効値には様々な形式(文字列, null …)があるが、全ケースのテストは現実的でないのでは?
A. テストだけで担保しようとせず、実装が適切か見直してみましょう。
対応例) 整数値以外は許可しない
の仕様で問題がないか確認 → 型・形式のバリデーションを追加
3.3 デシジョンテーブル
デシジョンテーブルとは、条件と結果(操作)の関係を2次元の表形式で整理する手法です。
ケース1 | ケース2 | ケース3 | ケース4 | ||
---|---|---|---|---|---|
条件 | 条件1 | Y | Y | N | N |
条件2 | Y | N | Y | N | |
操作 | 操作1 | X | – | X | – |
操作2 | – | – | – | X |
条件 は、if文やswitch文等の分岐条件に該当します。
例)入力済み or 未入力
, 形式不備あり or なし
Y:条件が真(True)であること ※Tや1と記述することもある
N:条件が偽(False)であること ※Nや0と記述することもある
–:条件が動作に影響しない ※N/AやDCと記述することもある
操作 は、実際に行われる処理に該当します。
例)登録実行
, エラー表示
, 通知送信
X:アクションが発生すること ※YやT、1、○などと記述することもある
–:アクションが発生しないこと ※N、F、0などと記述することもある
例として、ECサイトの割引適用について考えます。
下記の仕様のとき、ケース数はいくつになるでしょうか。
- 会員なら自動で割引
- 会員限定クーポンが入力されていたら追加で割引
- 会員限定クーポンは会員のみ使用可
- 非会員が会員限定クーポンを入力時はエラー表示(クーポンは会員のみ使用可の旨)
条件は会員か
, クーポンを使用しているか
の2つ
操作は会員割引
, クーポン割引
, エラー表示
, 割引なし(通常金額)
の4つです。
分岐数は各条件の組み合わせ数なので、2^2 = 4ケースですね。
これをデシジョンテーブルを用いて算出してみます。
割引処理のデシジョンテーブル
ケース1 | ケース2 | ケース3 | ケース4 | ||
---|---|---|---|---|---|
条件 | 会員 | Y | Y | N | N |
クーポン入力 | Y | N | Y | N | |
操作 | 会員割引 | X | X | – | – |
クーポン割引 | X | – | – | – | |
エラー表示 | – | – | X | – |
単純な表で、条件と操作の組を視覚化することができましたね。
テストでは、各ケースに対応する操作の挙動を確認すれば問題ありません。
条件によって操作が分岐する場合は、
デシジョンテーブルを用いてビジネスルールをまとめてみましょう。
Q&A
Q. 条件は必ずbool値じゃなきゃいけないですか?
A. enumのような複数要素を持つものでも問題ないです。
ケース1 | ケース2 | ケース3 | ケース4 | ・・・ | ||
---|---|---|---|---|---|---|
条件 | 会員種別 | 一般 | 管理者 | ゲスト | 停止済み | ・・・ |
Q. 条件の数が増えると指数関数的にケースが増えるのですが、どうすればいいですか?
A. おっしゃるとおり、2値条件の場合、組み合わせの数は 2ⁿ(n = 条件数)と指数関数的に増加します。仕様や目的に応じて、次のような工夫で現実的な範囲に絞り込むことが重要です。
-
不要な組み合わせの除外
→ 現実には発生しない、または使用上矛盾する組み合わせを排除する。 -
条件のグルーピング
→ユーザー種別 = ゲスト/一般/管理者
を「認証済みかどうか」の2値に統合するなど、意味的に近い条件をまとめる。 - ペアワイズ法(後述)との併用
なお、デシジョンテーブルは主に「条件と動作の整理・視覚化」に役立つツールであり、網羅的なカバレッジを保証するための手法ではないことにも注意が必要です。
ケースの除外を行う場合は、必ず人間の思考が介入します。
考慮漏れが発生しないよう注意しましょう。
3.4 ペアワイズ法
上記Q&Aのケースのように、一般的にすべてのケースを網羅することは現実的ではありません。
この対策として、統計学的な視点で、不具合が発生しやすいケースに絞ってテストを実施するという考え方があります。(過度なケースの圧縮は漏れに繋がるので注意)
ペアワイズ法では、「ほとんどの不具合は 2つ以下の因子の組み合わせ で引き起こされる」という調査結果を根拠に、2つの条件のすべての組み合わせを網羅する 手法です。
例として、3条件×各3値 のパターンを考えてみます。
- ブラウザ:
Chrome
,Firefox
,Safari
- ログイン状態:
ゲスト
,一般
,管理者
- 画面サイズ:
PC
,タブレット
,スマホ
通常の考え方では、総ケース数は 3^3 = **27ケース**です。
No | ブラウザ | ログイン状態 | 画面サイズ |
---|---|---|---|
1 | Chrome | ゲスト | PC |
2 | Chrome | ゲスト | タブレット |
3 | Chrome | ゲスト | スマホ |
4 | Chrome | 一般 | PC |
5 | Chrome | 一般 | タブレット |
6 | Chrome | 一般 | スマホ |
7 | Chrome | 管理者 | PC |
8 | Chrome | 管理者 | タブレット |
9 | Chrome | 管理者 | スマホ |
10 | Firefox | ゲスト | PC |
11 | Firefox | ゲスト | タブレット |
12 | Firefox | ゲスト | スマホ |
13 | Firefox | 一般 | PC |
14 | Firefox | 一般 | タブレット |
15 | Firefox | 一般 | スマホ |
16 | Firefox | 管理者 | PC |
17 | Firefox | 管理者 | タブレット |
18 | Firefox | 管理者 | スマホ |
19 | Safari | ゲスト | PC |
20 | Safari | ゲスト | タブレット |
21 | Safari | ゲスト | スマホ |
22 | Safari | 一般 | PC |
23 | Safari | 一般 | タブレット |
24 | Safari | 一般 | スマホ |
25 | Safari | 管理者 | PC |
26 | Safari | 管理者 | タブレット |
27 | Safari | 管理者 | スマホ |
これに対し、ペアワイズ法を適用し、すべてのパラメータペアが少なくとも一度は出現するような組み合わせを作成してみます。
No | ブラウザ | ログイン状態 | 画面サイズ |
---|---|---|---|
1 | Chrome | ゲスト | PC |
2 | Chrome | 一般 | タブレット |
3 | Chrome | 管理者 | スマホ |
4 | Firefox | ゲスト | タブレット |
5 | Firefox | 一般 | スマホ |
6 | Firefox | 管理者 | PC |
7 | Safari | ゲスト | スマホ |
8 | Safari | 一般 | PC |
9 | Safari | 管理者 | タブレット |
総ケース数を 9ケース まで圧縮できました。
今回は27ケースのものが対象でしたが、条件が多数存在する場合にペアワイズ法の真価は発揮されます。
パラメータペアの作成には様々な手法がありますが、
公開されているツールを用いることが最も手軽かと思います。
一例を載せておきます。
-
PICT(Pairwise Independent Combinatorial Testing tool)
Microsoft社が開発したソフトウェアテストツール。(MIT License)
https://github.com/microsoft/pict
テストケースが膨大になる場合、ペアワイズ法を用いて適度にケース数を圧縮してみましょう。
統計学的に、不具合が含まれやすい ケースであり、
全ての不具合が含まれているわけでは無い ことに注意しましょう。
過度なケースの圧縮は、漏れに繋がります。
3.5 状態遷移テスト
状態遷移テストは、システムの「状態」と、状態を変化させる「イベント(入力や操作)」の組み合わせに着目したテスト手法です。
ユーザーの操作や内部処理によって状態が変化するようなアプリケーションにおいて、どの状態からどの操作を行うとどう遷移するかを状態遷移図に整理し、それに基づいてテストケースを設計します。
状態ベースで考えることで、一連のフローや異常系の流れも見落としにくくなるため、ログイン処理やステータス管理、ステップ進行型のUIなどに非常に有効です。
デシジョンテーブルと同様に、情報の整理・視覚化が目的であり、
カバレッジを担保できるものではありません。
内部状態の遷移が複雑な場合は、状態遷移図でフローをまとめてみましょう。
3.6 ドメイン分析テスト
同値クラステストや境界値テストでは、ある一定範囲内での値をとる独立した変数のテストを行いました。
これを拡張し、同時に複数の変数のテストを行う手法がドメイン分析テストです。
複数の変数に対して境界の判定をする ということは、n次元(n:変数の個数)空間上での、ある領域と座標の関係性を調べる という操作と同義です。
しかし、この考えで進めてしまうと、3変数以上の場合の図示が困難(実質不可)になってしまいます。
そこで、ドメイン分析テストでは下記のポイントという概念を用いた、ドメインテストマトリクスという2次元の表に表現することで、n次元変数の相互関係を視覚化します。
- on ポイント : 境界上の値
- off ポイント : 境界に隣接する値
- in ポイント : 全ての境界条件を満たし、境界上にはない値(任意の有効値)
- out ポイント : いずれの境界条件も満たさない値(任意の無効値)
offポイントに有効値・無効値のどちらを取るかは、onポイントの値を見て判断します。
- onポイントが有効値(=)→ 無効値
- onポイントが無効値()→ 有効値
例として、下記のチルド宅配パックの規定について考えてみます。
チルド宅配パックの大きさの最大限は、長さ100cm以下、幅100cm以下でかつ長さ・幅の合計が150cm以下です。また大きさが長さ、幅が共に1cm以上の荷物が宅配の対象となります。
まず、条件を抜き出します
- 長さが1cm以上100cm以下 ・・・①
- 幅が1cm以上100cm以下 ・・・②
- 長さと幅の合計が150cm以下 ・・・③
大枠、”長さ” と “幅” の2変数について分析を行えば良さそうですね。
①②については、それぞれのパラメータに対して境界値テストをすれば良さそうです。
では③はどうでしょうか。合計値を聞かれているので、単一のパラメータに対する境界値テストでは確認できませんね。③を表す境界について境界値テストをする必要があります。
グラフ化した条件に対して境界値を求めていきましょう。
ドメイン分析では、
確認対象のパラメータに対して不等式条件の場合はonポイントとoffポイントの計2つを選び、確認対象ではないパラメータは有効になる値(inポイント)を任意に選択します。
等式条件の場合は、onポイントとその両脇のoffポイントの計3つを選び、確認対象ではないパラメータは有効になる値(inポイント)を任意に選択します。
- 長さ1cm以上
→ onポイント:1
, offポイント:0
, inポイント:50
- 長さ100cm以下
→ onポイント:100
, offポイント:101
, inポイント:50
- 幅1cm以上
→ onポイント:1
, offポイント:0
, inポイント:50
- 幅100cm以下
→ onポイント:100
, offポイント:101
, inポイント:50
- 合計値150cm以下
→ onポイント:150
, offポイント:151
, inポイント:100
これを基にドメインテストマトリクスを作成します。
ケース(列)毎にテストを実施することで、全ての条件の境界値テストを実施することができます。
複数の変数を同時にテスト可能(相互作用のある複数の条件式が存在)な場合は、ドメインテストマトリックスで変数間の相互関係を整理してみましょう
Q&A
Q. 直感的に理解しづらいのですが、理解するためのコツはありませんか?
A. 本質的には、着目していないパラメータを inポイント(任意の有効値)で固定し、対象のパラメータについて境界値テストをしているだけです。
その上で、重複しているケースなど無駄なものを排除していくとマトリクス上に現れるケースと一致するはずです。
Q. なぜ outポイント を使用しないの?
A. 有効な値(正常値)は複数同時に確認できるが、無効値(異常値)は、テスト時に複数同時に使用してしまうと、どの値が異常なのかを特定できなくなるためです。
入力項目が複数ある場合、異常値を同時に入れると異常の要因を特定できない(曖昧になる)ため、ドメイン分析では正常値のみを使用し、異常値のテストは別途実施するのが通例です。
例)x ≧ 10
、y ≧ 10
という条件において、x = 9
, y = 9
を入力する。→ x,yのどちらを検知したかわからない
ドメイン分析は相互作用のある要素間のテストを行うため、異常値(out)をケースに含めてしまうと正しく異常値のテストができなくなります。
別途、異常値用のテストケースを作り、ドメイン分析のケースの外で検証するようにしましょう。
Q. 不等式の場合でも、offポイントは2ケース選択すべきでは?
A. おっしゃる通りだと思います。同値分割の思想に従うのであれば、無効値は2ケース採用するべきかと思います。多くの書籍や記事で解説されているものが片方のケースに限定しているのは、効率性やケースの増加を防ぐ目的だと解釈しています。
x >= 10
の条件を誤って x > 10
としてしまった場合、このバグを発見できるのはonポイントである10
のみであり、offポイントである9
, 11
は補完的なテストでしか無いです。
ケース数とのトレードオフになりますが、安全側に倒す(境界の両側を確認する)という点では off を2つ取ることは個人的にはありな手法だと思っています。
4. ホワイトボックステストの手法
4.1 前提
ホワイトボックステストを行う上で、下記3つの観点は念頭においておくべきです。
- テストパス数が膨大なため全ケースの網羅は現実的でないこと
- テストケースによっては、データに依存した欠陥を見つけられないこと
- モジュールの制御フローが、仕様書の要件を満たしていることを前提としていること
テストパス数が膨大なため全ケースの網羅は現実的でないこと
ブラックボックステストの章でも述べましたが、一般的にカバレッジ100%の網羅は現実的ではありません。
テスタブルな実装なのであればできないこともないかもしれませんが、基本的にはある程度の取捨選択が必要です。そのうえで、どのように精度を担保するかが重要になります。
テストケースによっては、データに依存した欠陥を見つけられない
例えば、p = q / r
のような実装があったとき、r = 0
以外の場合は正しく動作するはずです。また、DBから値を取得するような場合でも、永続化層の作りが悪かった場合は意図しない値が取得される可能性もあります。仮にケースを全て網羅できたとしても、必ずバグを発見できるとは限りません。
モジュールの制御フローが、仕様書の要件を満たしていることを前提としていること
ホワイトボックステストでは、書いてあるコードを正としてテストをします。そのため、もし前提となる実装自体が仕様と乖離している場合、制御フローの誤りを検出することができません。
気休めに思えるかもしれませんが、上記のような観点を持ち続けることで、1件でも多くのバグを検出できる可能性を高めることができます。
4.2 制御フローテスト
“制御フロー“、つまりモジュール内の実行パス(分岐・繰り返し・条件)を対象にパス(ケース)を識別し、それらを網羅するようにテストを行います。
制御フローテストには、以下のような 網羅度 の概念があります。
網羅基準 | 概要 | 特徴 |
---|---|---|
命令網羅 | 全ての命令を1回以上実行 | 最も精度に欠けるが基本の考え方 |
分岐網羅 | 全ての条件分岐(true/false)を1回以上通る | 分岐の有無が検証できる |
条件網羅 | 各条件がtrue/falseを1回以上取る | 結合条件の内訳が確認できる |
条件/判定網羅 | 各判定結果と各条件がtrue/falseを1回以上取る | 組み合わせの網羅ができる |
パス網羅 | 全てのパスを通る | 理想的だが、現実的ではない |
※呼び方は様々あります。
基本情報技術者試験 等で学んだことがある方も多いのではないでしょうか。
正直、本記事の主題に合わせるのであれば、全てのパスを検証すれば解決なのですが、前提でも述べた通りこれは現実的ではありません。
我々が意識すべきことは、
- 正しく実装内のパスを抽出してケース化する
- 可能な限り高い網羅基準を目指す
- テスタブルな実装をする
ぐらいだと思います。
正しく実装内のパスを抽出してケース化する
どんなに仕様通りの正しい実装をしていたとしても、テスト対象を誤ってしまえば元も子もありません。図示をする、パスの自動生成ツールを使う 等、人間の脳を過信せず機械的に算出する動きをしましょう。
場合によっては、ブラックボックステストの章で紹介した デシジョンテーブル や ドメインテストマトリクス のような表を用いても良いかもしれません。
可能な限り網羅度の高い網羅条件を目指す
言葉の通りです。
100%網羅は難しくとも、可能な限り100%を目指すことは可能です。
時間とのトレードオフにはなりますが、より高い網羅度を目指してテストを行いましょう。
テスタブルな実装をする
そもそもテストしやすい実装をしてしまえば良いという発想です。
今回の記事の軸にはそぐわない内容かもしれませんが、テストをしやすい実装は結果的にテスト漏れを減らすことに繋がります。テスト駆動開発や責務の分離の概念も、この考えに則したものかもしれませんね。
余談
ちなみに弊社では、コードの可読性やテストのしやすさを保つために、GitHub Actions を用いて CI/CD パイプラインを構築し、コードの push ごとに自動で静的解析やテストを実行しています。
循環的複雑度やコードフォーマットといった観点も常にチェックされるため、一定の品質基準を物理的に担保できています。こうした仕組み化も、テスタビリティや品質を保つ上で有効な手段と言えるでしょう。
4.3 データフローテスト
制御フローでは、「どのように処理が流れるか」を重視しましたが、データフローテストでは「変数がどのように使われているか」に焦点を当てます。
主に、変数の定義(def)と使用(use)の関係を追跡し、「定義した変数が未使用ではないか」「未定義の変数を使っていないか」といった データの誤使用 を洗い出すことが目的です。
データフローで追跡するもの
用語 | 説明 |
---|---|
定義(def) | 変数に値を代入する処理(例: $x = 5;) |
参照(use) | 変数を読み取って使う処理(例: if ($x > 0)) |
無効化(kill) | スコープ終了や再代入で、以前の値が無効になる状態 |
プログラムの全体で「定義 → 使用」というパスを辿り、それが正しく成立しているかを確認するのが基本的な考え方です。
データフローの網羅基準
制御フローテストと同様に、データフローテストにも網羅度の概念があります。
網羅基準 | 説明 |
---|---|
定義-使用(DU)パス網羅 | 変数の定義から参照まで、すべてのパスをカバー |
定義-条件使用(DC)網羅 | 条件分岐内で使用されるパスをすべて網羅 |
定義-全使用(All-Uses)網羅 | 計算式・条件式を含むすべての使用を網羅 |
定義-全パス網羅 | 定義から到達可能なすべてのパスを網羅(非常に重い) |
例
$x = 5; // def(定義)
if ($x > 0) { // use(条件式で使用)
echo $x; // use(出力で使用)
}
上記の場合だと、$xが定義されてから、それが使われている(use)箇所が2箇所ありますね。
なので、この両方に対応したテストケースが必要になります。
本記事の趣旨との関連
本記事の目的は「思考の介入を減らし、機械的にケースを洗い出すこと」でした。
その観点から見ると、データフローテストの有効性は以下の点にあります。
- 制御フローでは検出しにくい変数の使い忘れや誤使用を拾える
- 変数ごとに処理のフローを可視化するため、ロジックの中で重要な箇所を自然とあぶり出せる
- IDEや静的解析ツール(PHPStan、Psalmなど)である程度の自動チェックが可能
補足
ただし、実務では以下のような限界にも注意が必要です。
- PHPのような動的言語では、静的にデータフローを解析するのが困難
→ 対策としては、型を明示したり、静的解析ツールの設定(PHPStanのレベル上げなど)を活用するのが有効です。 - すべての変数に対して網羅テストを行うのは現実的ではない
→ 優先順位をつけ、業務ロジック上重要な変数に絞って実施するのが現実的です。
データフローテストは、制御フローテストだけでは拾いきれない不具合を補完する手段として有効です。
特に、人間の直感だけに頼らず「変数ベースで機械的に抜け漏れを潰す」という意味ではテスト漏れ対策に有効な手段だと思います。
5. 終わりに
繰り返しになりますが、カバレッジを100%網羅したテストは現実的ではありません。
それは、テストケースの膨大さや、独自の仕様・業務知識の影響による機械化の困難性がによるものです。
ある程度のケースの圧縮はどうしても発生してしまうものですが、その意思決定を機械的・効率的な手法に代替できれば、きっとテストの精度は向上すると思います。
100%担保はできなくても、限りなく100%に近づける努力はできます。
自衛していきましょう。
6. 参考
【書籍】
【記事】
Views: 0