この記事の対象者
- プロジェクトでテストを書いている。(書いたことある)
- テストが重要らしい事は知っているが、テストの恩恵をそこまで実感できていない。
- 結局手動テストに依存したバグフィックスをしている。
はじめに
私はテストの設計手法、実装に関する知識は多く持っていましたが、知らなかったことはテストの考え方でした。
テストが重要らしいことを知っている人は多いと思います。 しかし、実際に恩恵を実感できていない人もいると思います。
事実、 テストが重要だと発信している人 と、 テストが重要らしいことを知っている人がいます。
後者の人は、とりあえずテストを書く事ができます。しかし、テストに時間を割く割りに、最終的には手動テストでバグを発見することに依存している事も多いかなと感じます。
世間ではテスト書くのが当たり前、テストは重要!という風潮であるのに、何故テストが重要であると実感できないのでしょうか?? どうすればテストが有効活用されているプロジェクトにすることができるのでしょうか?
今回の記事では、そういった問題に対して小手先ではなく、実践でどうやって考えるか?という思考回路を説明したいと思います。
例えば、以下の様なケースを考えてみます。
ケーススタディ
class SampleController
def index
# do something
end
end
このアクションの中には、A,B,C,D,E,F,G,H,I,Jという10個のメソッドが呼び出され、それぞれ(0, 1)を返し、1024通りの振る舞いが存在します。
ここで、 以下の状態の時に正常な振る舞いをしないというレポートが上がってきました。
レポート1
A -> 0
B -> 1
C -> null
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> - 2
レポート2
A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1
バグを再発させないためには、どういうテストをするべきだったでしょうか??
ここで、単体テストばかり書いているプロジェクトでは、レポート1のバグを防ぐことはできても、レポート2のバグを防ぐことは難しい場合があるでしょう。
こちらのケースを元に、どういうテストケースを書くべきなのか?ということについて解説していきたいと思います。
前提
いきなり方法論に入るのではなく、そもそもテストの目的は何か? 理想的なテストとは何か? 理想的なテストの問題点は何か?という点において解釈を統一したいと思います。
- テストの目的
- バグを防ぐにはどうすべきか?
- 理想のテストとは?
- 理想のテストの問題点
テストの目的
テストの目的は、
- バグを発見すること
- 品質を保証すること
- 品質を改善すること
と言われています。この中で一番重要な目的は 1. バグを発見する ことです。
つまり、極論を言えば、バグの無いアプリケーションが理想であり、その理想に近づくための良い手段の一つとしてテストがあります。
バグを防ぐにはどうすべきか?
仕様、プログラム設計、テストという3つの領域にわけられるかなと思います。
仕様、プログラム設計が悪ければ、テストも大変になります。
ですから、スパゲッティコードはリファクタリングしてからテストしてください。
ですが、今回はテストの話です。
理想のテストとは
では、理想的なテストとはどういうテストでしょうか?
それは、全てのユースケースが正常に動くこと を証明するテストです。
つまり、100通りの使い方があれば、100通りのレスポンスが正しいかどうかテストすれば完璧にバグを発見できます。
先ほどのケースを用いて解説していきます。
ケーススタディ
class SampleController
def index
# do something
end
end
このアクションの中には、A,B,C,D,E,F,G,H,I,Jという10個のメソッドが呼び出され、1024通りのユースケースが存在します。
また、それぞれのメソッドは全て、(0, 1)を返すとします。
ここで、 以下の状態の時バグが発生したというレポートが上がってきました。
A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1
回答(理想的な立場)
まず、組み合わせによってバグが起きているのか、単体のメソッドがバグを起こしているのかわかりません。
しかし、今回はそういった状況を無視して理想的な立場でテストケースを考えみます。
理想的な立場では、全ての組み合わせが正常であるかをテストします。
つまり、2 ^ 10 = 1024通りのテストをすればバグは完璧に発見することができます。
理想的な立場の問題点
ユースケースが1024通りもある機能なんて化物ですが、、現実的に普通のアプリケーションの一つのアクション(機能)に対して数100通りのテストをする事は不可能です。また、複数のアクション(機能)が結合していくと、無限のテストケースが必要になってきます。ここでの問題点は、 全てのユースケースをテストする事は工数的に不可能であるということです。(当たり前ですが)
現実的なテスト設計の方法
理想的なテストを目指せば目指すほど指数関数的に工数が膨らみます。
ですからソフトウェアテストの世界では、
いかに費用対効果の高いテストを書くことができるか?
ということが命題になってきます。
では、費用対効果の高いテストはどうすれば書けるのでしょうか。
それは、 バグの出やすいコードに対してしっかりテストを書くことです。
逆を言うと、理想的なテストから重要でない条件を間引くことです。
では、これからどうやって理想的なテストケースから、現実的なレベルに間引いていく3つの方法を解説します。
- 方法1:独立している要素を間引く
- 方法2:重要度の低いパターンはテストしない
- 方法3:統計情報に基づいて組み合わせを絞る
- まとめ
方法1:独立している要素を間引く
この手法はテストを現実的なものとする、最もベースとなる考え方です。
ケーススタディ
class SampleController
def index
# do something
end
end
このアクションの中には、A,B,C,D,E,F,G,H,I,Jという10個のメソッドが呼び出され、1024通りのユースケースが存在します。
また、それぞれのメソッドは全て、(0, 1)を返すとします。
命題1 : 「1024通りのユースケースは全て正常」 ならば 「10個のメソッドは全て正常」
命題2 : 「10個のメソッドは全て正常」 ならば 「1024通りのユースケースは全て正常」
命題1と命題2はそれぞれ真か偽かどちらでしょうか?
回答
命題1は「真」、命題2は「偽」です。(関数型言語を使うと後者でも真となるようなプログラムを書けるらしいですが)
前者は、2 ^ 10 = 1024通りのテストをする必要があり、
後者は、2 * 10 = 20通りのテストが条件です。
これらの命題が、 単体テストだけではバグを発見するためには不十分である という証明です。しかし、実際には間引いても十分な場合があります。
例えば、10個のメソッドの内、DとHのメソッドは完全に独立している(他のメソッドに対して影響を与えない)場合、DとHは 0 or 1 を返すことを証明できていさえすれば問題ありません。
また、EとFもそれぞれ影響のスコープが狭く、組み合わせのテストに必要が無いと判断できます。
つまり、ユースケースの指数は10から6に減り、単体テストが4つ増えます。
2 ^ 6 + 2 * 4 = 70通り
こうやって、独立している組み合わせを間引いていきます。
ポイント
独立しているメソッドは、単体テストで十分であり、組み合わせの指数から引く。
方法2:重要度の低いパターンはテストしない
これは理論的に決定できるものではありません。ケースバイケースですが、考え方を知るだけで誤差は許容される範囲で収まるでしょう。
ケーススタディ
class SampleController
def index
# do something
end
end
このアクションの中には、A,B,C,D,E,F,G,H,I,Jという10個のメソッドが呼び出され、1024通りのユースケースが存在します。
また、Aというメソッドはライブラリーで提供されている認証機能です。認証の場合1 認証でない場合0を返します。
Fというメソッドは、'red', 'green'という値を返し、ボタンの色を変えます。
Hというメソッドはユーザーの入力した情報から、プランAオブジェクト, プランBオブジェクトを返します。
この中では、テストしたほうが良いケースと、テストしなくても許されるケースという序列があるはずです。では、どうやってテストの優先順位を決めるべきでしょうか??
回答
(重要度) × (バグ混入リスク) というマトリックスで判断します。
※ 第3回:テスト項目の絞り込み:重み付けは必ず数値で表そう
という記事で、ソフトウェアの重鎮の方々が意見を述べています。
重要度とは、ユーザー的に重要、影響範囲が広い等の要素があります。
バグ混入リスクとは、仕様的に、ソースコード的に複雑な場所ほど高くなります。(結合度が高い場所)バグ混入リスクは、メトリクス解析をすることである程度定量的に判断することができたりします。(メトリクス解析については後ほど)
つまり、今回のケースだと、Aは信頼されているライブラリで提供されており、バグが発生するリスクは低い。
F影響範囲が狭く、ユーザー的にもそこまで重要でない。
Hはアプリケーションの価値そのものであり、ロジックも複雑である。
つまり
このようなマトリックスになり、Aは単体テストをする必要がなく、Fは組み合わせのパターンにいれる必要がないと判断できます。
ポイント
(重要度) × (バグ混入リスク) を考え、単体テストがいらないコード、組み合わせのパターンに入れる必要が無い箇所を間引く。
方法3:統計情報に基づいて組み合わせを絞る
ここまでで、どういうテストケースを間引いよいのか?という基本となる考え方を知りました。
しかし、人間の力では一つの機能に対して、数十個のテストケースすら厳しいものがあります。
そこで、テストエンジニアの方々はどういう時にバグが起こりうるか?ということを統計情報にしました。
すると、 2パラメータ間までで発生する不具合が7~9割を占める。 という統計結果が現れました。このデータを元に考えられたテスト設計手法が ペアワイズ法 と呼ばれる手法です。
ケーススタディ
class SampleController
def index
# do something
end
end
このアクションの中には、A,B,C,Dという3個のメソッドが呼び出され、8通りのユースケースが存在します。
ペアワイズ法とは以下のとおりです。
組み合わせテスト技法の1つであるオールペア(ペアワイズ)法では、すべてのパラメータについて少なくとも、2パラメータ間での値の組み合わせが網羅されるようにテストパターンを作成する。
と書かれています。
つまり、2つのメソッドの組み合わせが全て網羅されていればおっけーです。
例えば、A,B,C,Dというメソッドをペアワイズ法でテストケースを考えてみると以下のようになります。
A B C D
1 0 0 0
1 1 1 1
0 1 0 1
0 0 1 1
0 1 1 0
ペアワイズを作るツールが有るらしいので使ってみるのもありかもしれません。
組み合わせテストケース生成ツール 「PictMaster」 とソフトウェアテストの話題
まとめ
方法1、独立しているパターンを見つけましょう。
方法2、本当にテストするべきかマトリックスで考えましょう。
方法3、賢く統計情報に基づいて組み合わせの数を減らしましょう。
そうすれば現実的な組み合わせになると思います。
実践でテストを書くときに知っておくと良い知識
以上でベースとなる考え方は身に付けることができました。
しかし、今までのケースはある程度理想化されたモデルをベースにしたケーススタディです。
今までのケースでは、組み合わせが非常に明確でした。
しかし、現場のコードでは組み合わせの数は人によって違いが現れたりします。
また、人によってどういうコードにバグが潜んでいるのか?という解釈にもばらつきが生まれます。
しかしながら、プロジェクトで開発する時はなるべく共通の認識を持ちたいものです。
そこで、以下の項目について知識の解説をしたいと思います。(知っている項目は飛ばしてください。)
- カバレッジ率
- 同値分割と境界値分析
- メトリクス解析
カバレッジ率とは、経路の組み合わせを数える方法です。
同値分割と境界値分析とは、値のパターンを抽出する方法です。
メトリクス解析とは、どういうコードにバグが潜んでいるかを定量的に測定する手法です。
これらを知ることで、ある程度プロジェクト間で組み合わせ選択のばらつきや、優先順位のばらつきがなくなります。
カバレッジ率とは?
プログラム内の分岐をどの程度網羅したかを表す指標。
カバレッジ率には、C0, C1, C2というレベルがあります。
こちらのコードの例で説明します。
def my_method
if type1 == "A"
print("処理1")
else
print("処理2")
end
if type == "B"
print("処理3")
end
end
C0:命令網羅
C0レベルでは命令を網羅しているかどうかを判断します。
つまり処理1 ~ 処理3を一度通せばオッケーです。
def my_method
if type1 == "A"
print("処理1")
else
print("処理2")
end
if type == "B"
print("処理3")
end
end
テストケースは、以下の2つでC0レベルのカバレッジ率100%と言えます。
type == "A" TRUE, type == "B" TRUE
type == "A" FALSE, type == "B" TRUE
C1:分岐網羅
C1レベルでは、分岐を網羅してるかどうかを判断します。
つまり、全ての分岐を一度通せばオッケーです。
def my_method
if type1 == "A"
print("処理1")
else
print("処理2")
end
if type == "B"
print("処理3")
end
end
テストケースは、以下の4つでC1レベルのカバレッジ率100%といえます。
2 + 2 = 4通り
type == "A" TRUE, type == "B" TRUE
type == "A" FALSE, type == "B" TRUE
type == "A" TRUE, type == "B" FALSE
type == "A" FALSE, type == "B" FALSE
C2:条件網羅
C2レベルでは、分岐の組み合わせも全ても網羅しているかどうかを判断します。
これは理想のテストにかなり近いので大変です。
def my_method
if type1 == "A"
print("処理1")
else
print("処理2")
end
if type == "B"
print("処理3")
end
end
テストケースは以下の4とおりですが、実際には条件が増えるたびに指数関数的にテストケースが増え続けます。
2 * 2 = 4通り
type == "A" TRUE && type == "B" TRUE
type == "A" FALSE && type == "B" TRUE
type == "A" TRUE && type == "B" FALSE
type == "A" FALSE && type == "B" FALSE
同値分割と境界値分析
さて、ここまでで条件分岐の数によって組み合わせがカウントされることが分かりました。
しかし、まだもう一つ組み合わせの変数があります。それが値の取りうる範囲です。(値域)
ケーススタディ
以下のコードのテスト戦略について考えます。
# varが取りうる値はString, Int, nillのいずれか
def my_method(var)
var.your_method
end
カバレッジ的には、1回で十分です。
しかし、カバレッジ率だけ考えるのでは不十分です。
今回ならばString, Int, nillのケースを最低限テストしたいはずです。
カバレッジが経路の網羅ならば、今回は値の網羅です。
しかし、値の網羅は無限の通りがあります。(自然数だけでも)
では、どうやって網羅すればよいでしょうか??
同値分割
例のごとく、解説は他のわかりやすい記事に譲ります。
境界値分析
その名の通り、境界値をテストすることです。
メトリクス解析とは
ソフトウェアを静的に解析し、様々な観点から品質を定量評価するものです。
この考え方を知ることで、 どういうソースコードにバグが多いのか? ということがある程度わかります。 詳しい説明は以下に参考リンクを貼っておきますが、循環複雑度と呼ばれる指標だけ説明します。
循環複雑度とは?
ソースコードの経路の数を数えることで、複雑度を示す方法です。
とは言え、こちらの記事が圧倒的にわかりやすいのでぜひ見てください。
\バグだー!/ システムに巣食うバグさんからみた快適さ、循環複雑度( Cyclomatic Complexity ) とは? \バグだー!/
参考リンク
メトリクス解析とは
Ruby on Rails | metric_fuでメトリクス分析
静的コード解析ツールの MetricFu は何を見ているのか
各言語のメトリクス解析ツール
初めてのソフトウェアメトリクス(前編): ソフトウェアの品質を数値化して確かめる
テストケースを選択する手法を選びたい時に読む。
「知識ゼロから学ぶソフトウェアテスト」を読んで [ホワイトボックステストから探索的テストまで]
Q&A
だいぶ書きました。しかし、プロジェクトメンバー全員がテストの品質を保つのは難しいものです。
そこで、今までで出てきた論点などをQ&A方式で書いていきたいと思います。
質問1:ユニットテストとE2Eテストはどうやって使い分ければよいの?
理想は全ての関数、メソッドの振る舞いに対して単体テストし、
それらの全ての組み合わせをE2Eテストでテストすることです。
しかし、現実的にそれは厳しいです。
そこで、複雑度の低い関数、メソッドは単体テストしない。
独立している組み合わせは、テストケースから間引く。
という考え方でテスト設計します。
随時更新