「単体テスト」再入門! 開発の現場でバグを確実に洗い出す最適な手法と、テストケースの作り方
単体テストの定義から手法、未来の展望までを、日本におけるソフトウェアテストの第一人者・高橋寿一さんが解説します。
ソフトウェアのテストにおいて、最初のフェーズである単体テスト。若手Webエンジニアの中には、いきなり単体テストを任されて戸惑った方もいるでしょう。仕方なく現場で踏襲されているやり方に従っているだけ、ということもあるのではないでしょうか?
今回は、単体テストの定義から手法、未来の展望までを、日本におけるソフトウェアテストの第一人者・高橋寿一さんが解説します。
単体テストとは(各社ばらばらな単体テストの定義を再定義)
ソフトウェア開発において、用語の定義は非常に重要です。そこで、単体テストの手法を解説する前に、まずは単体テストの定義を再定義したいと思います。
さて、この「単体テスト」という言葉。会社や人によって意味するところが少しずつ違うな……と思ったことはありませんか? それも仕方ありません。単体テスト(Unit test)の定義が迷走してきた歴史は長く、昨日今日始まったことではないからです。
「単体テスト」という言葉が初めて世に出てくるのは、1970年の『Managing the Development of Large Software Systems』という論文に遡ります{$annotation_1}。
次の図でいう「CODING, UNIT TEST(コーディングと単体テスト)」が単体テストになります。
しかし日本で仕事をしていると、単機能のテスト(プリントできまっせ、URLジャンプしまっせ等々)も単体テストと言われるケースがあります。
先に書いたように、用語の定義は重要です。開発をスタートする前に、コードに対する確からしさを確認するのを単体テストと呼ぶのか、もしくは単機能に対するテストのことを指すのかを明確にした方がよいでしょう。
コードベースの単体テスト
「コードベースの単体テスト」とはなんだか聞き慣れない用語ですね。でもそういう言い方をここではしていこうと思います。
「単体テストとは何か」という厳密な定義はなく、ISTQBの用語集にもありません。昔はあったような気もしますが、今はありません。それほど曖昧な用語であり、私の参加する世界中の委員会でいつも揉めていました。
そこで本稿では、「コードベースの単体テスト」という用語を使うことにします。この用語を厳密に表現すると、「関数の網羅率を計測し、ロジックの確からしさを確認するテスト」ということになります。これはホワイトボックステスト手法とも言います。この定義に従って話を進めていきます。
単体テストの手法は、機能単位の単体テスト(ブラックボックステスト)と、コードベースの単体テスト(ホワイトボックステスト)の2つに大きく分かれます。コードベースの単体テストは、厳密なソフトウェア品質を求められる自動車や医療のソフトウェアでは必ず行われるテストで、ISO等々で行うことをルール化されています。
さらに、コードベースの単体テストの中には、命令網羅(C0カバレッジ)、分岐網羅(C1カバレッジ)といった、いくつかの手法があります。
ここではよく使われる命令網羅と分岐網羅を紹介します。
命令網羅(C0カバレッジ)
命令網羅テスト、またはC0網羅{$annotation_3}と呼ばれる網羅を説明します。命令網羅でのテスト基準は、少なくとも1回はプログラムの全ての命令文(ステート)を実行することです。例えば、以下のようなコードがあったとします。
if(con1 == 0){ x = x + 1; } if(con2 > 1){ x = x * 2; }
この場合、命令網羅テストでは、下記の全ての四角を通る経路をテストします。
テストケースは以下のようになります。
con1 = 0, con2 = 2
そうなると赤矢印の部分を通るテストが抜けます。はいそうです。命令網羅というのは不完全なテストなのでやってはいけません。
しかし多くの組織で、命令網羅で単体テストを達成しようとしています。コードの単体テストは労力のかかる仕事ですが、中途半端な命令網羅はせずに、次に紹介する分岐網羅をしっかり行うことをオススメします。
分岐網羅(C1カバレッジ)
分岐網羅またはC1網羅と言われるテストが、上記で示したC0網羅の問題を解決する網羅手法です。分岐網羅では、それぞれの判定条件がTRUE、FALSEの結果を少なくとも1回ずつ持つようテストケースを書きます。
条件分岐に比べて網羅性が高いため、ほとんどのケースではこの網羅手法で単体テストを行った方がよいと思います。
先のプログラム例では、次の2つのパターンでデータでテストすれば、テストが完了になります。
テストケース1:con1 = 0, con2 = 2
テストケース2:con1 = 1, con2 = 0
よくある(コードベースの)単体テストの間違い
よくある単体テストの間違いですが、「命令網羅や分岐網羅の網羅率が100%に達すればいいんだ!」と考える人が少なくありません{$annotation_4}。
上記のような簡単な例ならいいのですが、一般にソースコードは巨大で、各関数も数百行になり、ある関数からまた別の関数を呼び出している、といったケースもあります。なので、網羅率を100%に持っていくことはなかなか難しいです。
単体テストの目的は、コードロジックの正しさを「網羅」によって論理的に証明することにあります。そしてコードロジックが正しいと証明されれば、それは単体レベルの処理機能のバグがないということを意味します。
ここまでは、命令網羅・分岐網羅というコード経路の網羅手法を紹介してきましたが、「網羅」には、もう1つ別の視点があります。
それが、入力値の網羅です。入力値とは、関数に渡される引数、キーボードから入力される値といった、プログラムに対して与えられるデータです(これに対し、関数の戻り値、画面に出力する値といったデータを出力値といいます)。
単体機能のバグをなくすには、分岐網羅の網羅率を100%にするのがコスト面やスケジュール面で難しい場合でも、入力値のパターンを100%網羅し、それに対する期待値(入力値に対するプログラムの仕様通りの出力値)が正しいかチェックする必要があります{$annotation_5}。
しかし、現場の担当者は往々にして、期待値の確認をメンドウだと言って行わず、コードが網羅されていることで満足しています。そして、単体テストで見つけるべきバグを、次のフェーズである統合テスト6で発見し、品質担保で苦しんでる組織が多いのではないでしょうか?
コードベースの単体テストは、ほとんどのバグを見つけられる(個人的意見では、80%以上のバグを見つけられる)テスト手法です。しっかりテストをすることにより、後工程でのバグの数を著しく減らせます。単体テストでは分岐網羅に、より多くのテスト工数を割くことをオススメしています。
機能単位の単体テスト
ここまでは、コードベースでの単体テストを説明しました。次は「機能単位の単体テスト」を説明していきます。これはブラックボックステスト手法とも呼ばれます。
製品にそれほどの高品質を要求されない場合は、この機能単位の単体テストのみが実施されることが多いです。また、先に説明したコードベースの単体テストを実施したうえ、さらにUIから機能単位(特に複雑な機能)の単体テストを実施する場合もあります。
Webアプリケーションにおいて、一般的には「UIから何かを入力して期待する値が表示される」ことが単体テスト、とシンプルに考えがちです。しかし、複雑な機能についてもバグを見逃さないよう、また無意味なテストケースを膨大に書くことにならないよう、適切なテストケースを抽出することが重要です。
例:複雑なソート機能のテスト
複雑な機能の例として、今回は教科書的なつまらない単体テストの例ではなく、下のような、データをソートする機能確認を想定して解説していきます。
この機能では、各列の▽ボタンを押すと、その列をキーに昇順でデータがソートされるものとします。
もし、列内に同じ値がある場合(例えば、名前の列に同性が2人)は、その右隣の列(この場合は年齢)の値によってソートします。
テスト実施者に知らされている情報は、以上の仕様のみです。ブラックボックステストですから、コードのロジックには着目しません。
さて、たぶん初心者は▽ボタンを押して、年齢通りソートできてるな、入社年度ごとにソートできてるな、配置ごとにソートできてるな、といった具合に4つのボタンを押して、「単体機能確認できました!」と報告すると思います。
しかしテストのプロは違います。確実にこの部分からバグを見つけ出すために、最適な数のテストケースを作成していきます。
機能単位の単体テスト手法の基本は、次の2つです。
- 単機能境界値テスト
- 組み合わせテスト
境界値テスト
まず、境界値を考えてみましょう。簡単なところから年齢です。まず人間が生きられる年齢は0~150歳ぐらいですかねー。まあ0歳が入社できるかはおいといて。
なので、プログラムが0歳がエラーなく処理できるか、またその境界の-1歳がエラー処理できるかを見ます。上限の境界は150歳ぐらいですかねー、また1,000歳の場合エラー処理しますかねー、と境界を探りながらテストをしていきます。
ここで「1,000歳でも正常処理していいんじゃん!」という意見もあるかもしれません。しかし、大きい数の処理はコンピュータは苦手ですし、想定外の桁数は表示エリアからはみ出す可能性もあります。「このプログラムにおける、適切なエラー処理はどうあるべきなのか──例えば、1,000歳のときは正常処理されるべきか、それともエラーダイアログが表示されるべきか?」ということは、常に考える必要があります。
また、年齢にアルファベットが入っていた場合にどうなるか、というような確認も必要です。
もう1つ、データ件数の境界値も考える必要があります。データの件数が0のとき、1のとき、件数がとても多いとき、というのも境界値テストの条件になります。
組み合わせテスト
次に、組み合わせを考えていきましょう。テストにおける組み合わせとは、入力値など、与える条件の2つ以上の組み合わせを指します。
一般的に組み合わせを考える場合は、そこからバグが出やすいかどうかという検討が必要です。もしその検討をしないと、テストケース数が爆発してしまいます。
それではソートプログラムではどういう場合にバグが出るでしょうか? 少し複雑なデータで考えてみましょう。
組み合わせを考える場合は、1
、2
、n
を常に頭にいれる必要があります。データが1
つの場合にソフトウェアがちゃんと動いているかは、上記の単機能境界値で確認しました。次は2
です。
当然、同性が2人いた場合を考えなければならないので、同性の名前があった場合に、年齢が正しくソートできているかを確認します。下のようなデータを作成して、正しくソートできるかを見てみましょう。はい、名前の次のデータである年齢でソートされていますね!
続いて同年齢の場合のソートを確認してみましょう。下のようなデータを作成して、年齢の次の入社年度でソートされているかチェックします。
このように、2つの組み合わせでその機能がちゃんと動いているか確認していく必要があります。同じように、入社年度と配置の組み合わせ、配置と名前の組み合わせ、とテストケースを作成します。
皆さんの中にはひょっとすると、次のように複雑なデータを作って、いろいろテストを試したいと思われる方がいるかもしれません。「年齢」でソートしたとき、同年齢の場合は次の「入社年度」でソート、かつ「入社年度」が同じ場合は次の「配置」でソート、かつ「配置」が同じ場合は次の「名前」でソート。
次は、n
のソートを上記の図で見てみましょう。確かに、こういうソートはケースとしてはありえますが、これは書き始めると無限大の数になります。こんなテストケースを書いてバグを見つけることは、宝くじを当てるようなもので、あまり意味がありません。
なので、まず1
と2
を確実に網羅し、n
というテストケースを適切数(往々にして数個)書けば、品質的にはほとんど問題はありません。
それでもバグが出たらどうするんですか? って質問してくる方もいますが、いつも私は自信を持って「こんなバグはまず出ません。またほとんどの場合、バグは検出できません! 確率統計的に(経験則的に適当と思われるそれっぽい計算式を示し)、検出するには○○億円かかりますが、その費用かけます?」とマネージャに聞きます。いままで1人も「よしその費用をかけよう!」と言ってくれた人はいませんでした。
確かに、開発者が思いもよらない変なコードを書き、3つ以上の条件でのバグが出ることはありますが、それを防ぐための適切なテスト手法がないものも事実なので、諦めるしかありません7。
機能単体の単体テストにおける組み合わせのバグ検出確率というものは、コードベースの単体テストよりかなり低いという認識を持つ必要はあります。そしてまた、このようなバグを見つけるのはテスト担当者ではなく、開発者もしくはそのコードをレビューしているレビューワーだということを忘れてはいけません。
現代の巨大ソフトウェアでは、開発者自身もテスト担当者の意識を持つことが重要です。余談ではありますが、こういった単機能が複雑な場合には自動化テストをオススメします。まあ一晩中流しておけば安心感はかなり得られますよ。
ブラックボックステストとホワイトボックステスト
ここまで、コードベースでの単体テスト(=ホワイトボックステスト)と、機能単位の単体テスト(=ブラックボックステスト)を説明してきました。それでは、私の製品はどちらの単体テストをやるべきですか? 両方やるべきですか? という問いがあるでしょう。
しかし、現実世界において私の長いコンサルタント経験からしても、それを問われることが実は少ないことも事実です。なぜなら各社・各事業部で昔からあるやり方に疑問を持たず踏襲しており、現場の担当者はドラスティックな改善にあまり前向きではないからです。品質が悪くなれば、今まで踏襲してきたやり方でテストケースを追加するなどして、泥縄的にしのいでいます。
そうした状況に警鐘を鳴らすべく、「御社の製品は特にコード品質が悪いので、ホワイトボックスでの単体テストを追加したらどうでしょうか?」とコンサルタントとして顧客に提案することもあります。担当者からは「そんな時間も予算もありません、何年間も開発し続けた膨大なソースコードの単体テストを1からやるなんて土台無理です!」と一蹴されます。
それでも引き下がらず、「いえいえ、現代のソフトウェア理論では、ソースコード全体の20%程度をテストすれば十分な品質になる単体テスト手法もありますが、どうですか?」と提案したりもします。
グーグルはコードの品質向上のため「バグ予測アルゴリズム」を採用している - Publickey
だいたいの場合、その後顧客からの連絡がなくなります。残念ですが、これ、よくある会話の一幕なのです。要は、新たなテスト手法を追加したくないのが、日本のソフトウェア組織のようです。
コードベースでの単体テストをやるか否かは、過去の本番稼働において、コードベースの単体テストをやっておけば防げたはずの問題が発生したかどうかで判断すればよいでしょう。
ブラックボックステストで全てのバグを見つけられれば、ブラックボックステストよりコストのかかるホワイトボックステストをやる必要はありません。
とはいえ、一般的には、ブラックボックス・テストとホワイトボックステストで見つけられるバグの範囲は違っています。イメージでいえば下の図のようになります。
もしホワイトボックステストをはしょれば、バグがいくつか残ります。それが軽微なバグならばよい──例えば、無料スマホアプリで、たまに動かなくなってもかまわない──という場合は、この方法でも問題ありません。
それでは、自動車のソフトウェアはどうでしょうか? 最近の車はコンピュータだらけで、そのソフトウェアで問題が起きれば事故につながり、最悪ドライバーが死んでしまいます。なので、車のエンジンやミッションコントロールのソフトウェアでは、ホワイトボックステストが必須です。しかし、カーナビゲーションソフトウェアのほとんどはホワイトボックステストを行っていません。まあカーナビが故障したら困るけど、スマホの地図でなんとか代用できますしね。
まとめ
今回、単体テスト・ホワイトボックステスト・ブラックボックテストの効用、および適用方法について説明してきました。
Webや書籍を見ても、テスト手法を説明するものは多いですが、現場でどの手法を最適に採用するべきかを論ずる情報は多くありません。昔日の日立やNECや三菱といった、日本のソフトウェアを支えてきた会社には膨大な内部資料があり、そのデータを元にどういうテストを行うべきだという指針がありました。
しかし、日本の家電のソフトウェアが凋落し、GAFAがソフトウェアの世界を握るようになった今、私たちはテストの基礎技術を学んだうえで、先端的技術については彼らのやり方に追随するしかありません。ところが、基礎技術もおろそかになり、先端的技術の追随もうまくできないというのが、日本のソフトウェア産業の現状でもあります。
それでは、日本が世界を牽引する自動車産業のソフトウェアはどうであろう? という問いかけもあるでしょう。実際のところ、日本における自動車のソフトウェア開発現場の技術力は、GAFAのそれと比べ大きく遅れていると筆者は感じます。
今後、日本のソフトウェア品質領域で活躍する方々は、GAFAの技術をキャッチアップしながら自社ソフトウェアを作るという、大変な作業を強いられることと想像しています。そんな中でも、自社内で閉じた、あるいは継続されてきたやり方に流されず、「考え」「仮定し」「仮定をベースにした実験的な取り組みをし、データを取る」というような健全な品質活動にぜひ取り組んでほしいと思います。
高橋 寿一 (たかはし・じゅいち)
編集:大高友太郎(リブロワークス)
-
Managing the Development of Large Software Systems (Winston W. Royce, Symposium on Advanced Programming Methods for Digital Computers on 29 June 1956)↩
-
ISTQBは、世界最大のソフトウェアテストにおける資格団体です。↩
-
実はC0網羅という言い方は正しくなく、命令網羅と言わなければなりません。なぜC0という言い方がされだしたのかは、ながーいストーリーになるのでここでは書きません。↩
-
網羅率をどれくらいにするべきかというのは、コンサルタントをやっていていつも聞かれます。自動車などのミッションクリティカルなソフトウェアの場合は、100%と自信を持って答えます。
しかし、ミッションクリティカルではないソフトウェアでも、コードベースでの単体テストを十分に行うことは、後半工程のバグを減らすなど、ソフトウェア品質の向上に多いに役立ちます。そのような場合の網羅率は80%でよい、と私はやはり自信を持って言い切ってしまいます。実際は言い切れないんですが(笑)
筆者は数十年に渡るテスト関連の論文に目を通しています。しかし、まっとうな論文にはその網羅率のゴールは書いていません。もちろんISOやIEEEの規格にも書いていません。なぜなら、そう書いて致命的なバグが出ると責任を取らなければならないからです。
なぜ筆者は80%を主張するかというと、ソースコードの20%程度はエラーハンドリングの処理なので、そこまで単体テストで網羅する必要がないだろうと考えられるからです。実際にBoris Beizerという著名なテスト学者もそう言っています(彼の本にはもちろん書いていませんが、カンファレンスでの質問にはそう答えていました)。↩ -
入力値に対応する出力を網羅的にチェックすることは、機能単位のブラックボックステストであるとも考えられます。しかし、コードを網羅し、コードのロジックが正しいかを判断するためのテストがホワイトボックステストなので、ここではあえてコードベースのホワイトボックスと仮定させてください。厳密には確かに異論はありますが……。↩
-
「統合テスト」は「結合テスト」と同様の意味で使われますが、ISTQBの用語集には結合テストという用語はなく、統合テストと記載されているため本記事ではこちらを使います。↩
-
数学的感覚でいうと、2つやれば組み合わせが網羅できるので、理論上品質に問題ないと証明できます。そのため、組み合わせテストにはAll-pairという手法があります。たしかに、コードを書く人の技術不足などで、3つ以上の条件によるバグが発生することもあります。しかし3つの条件までチェックするとなると、ほとんどのソフトウェアで組み合わせテストケース数が爆発します。2つなら100テストケースなのが、3つになるとゼロが1つ増えて1000ケースになります。なので、現実的なソリューションとしては、2つまで頭で考えて理論的にカバーし、3つ以上に関しては感覚を信じ、いくつかのテストをするということになると思います。↩