Top Of Page テストの役割 テスト仕様 テスト方法 Basic Test Data Flow 同値分離 バグ予想 テストのバグ Bottom

単体テスト

プログラマによっては、というより、開発の方法によっては、単体テストとデバッグを同じ意味に使用している場合もありますが、ここでは別のものとして区別します。
単体テストとは、エラーを調べる手段で、デバッグとは、それを分析、診断してエラーの原因を取り除く手段とします。

また、趣味でプログラミングをしている人には「単体」という意味がピンとこないかもしれません。
通常、システムは複数の(大体の場合は多くの)プログラムによって構成されます。
単体テストとは、そのプログラム1つ1つが、きちんと設計されたとおりに動作するかを検査するものです。 このほかに、連結テストや運用テストなどと呼ばれる、プログラム同志の連携がうまくいくかどうかのテストをおこないます。
単体テストは、そういう意味ではテストの基本中の基本、外してはとおれないものなのです。

単体テストの役割

「テストが成功している」というのは、「プログラミング上のミス=バグが発見されている」ということです。
うまくいっているテストは、エラーを頻発させ、プログラムを壊してしまうかもしれません。
しかし、本来テストというのはそういうものなのです。
バグが見つからないテストでほっとしてはいけません。もしバグが見つからなかったらテストが効果的でなく不完全である証拠だと思ったほうがいいくらいです。

どんなにテストしても「バグがない」ということを証明することはできません。
どんなプログラマでも、最後には祈ります。
─ユーザーが発見するはずだったすべてのバグが、どうかテストですべて発見されていますように。
そう、そのためにこそテストをするのです。すべてのバグが取り除かれたことの証明はできませんが、ユーザーがプログラムを使用するそのパターンを網羅し、そのパターンにおいて不具合が発生しないことを(あるいは発生するが、それが致命的なものではないことを)確認する、それが単体テストの役割です。

また、テストを多くやったから品質が向上している、という考えがありますがこれはちがいます。
テストの結果は品質を示す指標ですが、回数を増やして品質が向上するとは限りません。
品質を効率よく向上するには、品質を向上させる手段を別に考案する必要があります。
その手段は、いままでにわたって何度も述べてきました。
整然とした問題分析、直接的でわかりやすい実現手段、よい名前、よい使い方─など。
これらを抜きにして、品質の効果的な向上は望めません。
テストは、いわばその手段の結果の検証なのです。

テスト仕様の意義

テストのパターンを記入したものを「テスト仕様(書)」といいます。
このテスト仕様にそってテストするわけです。
これをせずにいきなりオモムロにテストし始めるひとがいますが、これはいただけません。

たとえばプログラムをリリースしてから、バグが発見されてしまったとします。
ここでテスト仕様書があった場合、そのバグが起きるパターンがテスト仕様にあるかどうかをまずチェックします。
そこになければ、「テストパターン漏れ」で、そのパターンはテストされていなかったことがわかるわけです。
もちろん「テストパターン漏れ」はよくないことですが、テストパターン漏れならばそのバグが潜んでいたことについて合理的な説明がつきます。
逆にテストパターンにあった場合は、最後にそのパターンのテストを行った日からリリースの間に変更した個所にバグが潜んでいたことになります。
しかし、こういう判断はテスト仕様が明確に作成されていてこそです。
つまり、テスト仕様というのは、これだけのテストを行ったという証拠であり、そのプログラムをリリースするものとしての責任感の証明でもあるのです。

そして、テスト仕様がない場合、もっと深刻な問題も抱えます。
たとえば前述のようにバグが発生してしまったとします。
当然それを修正することになります。そして修正は終了しました。しかし。
しかし、その修正が、他の、いままで正常に動作していた部分に対して悪影響がないということをどうやって証明しましょうか。
テスト仕様があれば、もう一度そのテスト仕様にそってテストすれば、少なくとも今まで正常だった部分に対する証明ができるわけです。
しかしテスト仕様がないなら、いったいどんなテストをすればいいのでしょう。
ここでテキトーにテストしてしまうと、その修正で新たに潜んでしまったバグを発見できるとは限りません。

テスト仕様を作成するのは、プログラムの仕様を決める段階、あるいはプログラミングのなるべく初期の段階でやってしまいましょう。
メモリエラーなどの信頼性についてのチェックは、みおとされがちなので、早い時期にテストの対象にしましょう。
また、コーディングをしていると、おかしな仮定が頭の中に出来上がってしまうことがあります。そういったことを避ける為にも、なるべく早い時期に、テストパターンを決めるべきです。

単体テストの方法

というわけで、テストはヒッジョーに大事なことなのですが、しかし辛い作業でもあります。
また、がむしゃらにテストしても、効果的とはいえません。
やはり最小限の労力で、効果的にテストしたいものです。

でも、最小限の労力といっても…

これだけは譲れません。これを満たさないテストは、「最小限の労力」ではなく「手抜き」です。
そしてテストするときには、重要なパターンのテストをまずを行い、それから詳細な項目をテストするといいでしょう。
重要なパターンのテストが失敗するということは、もしかするとシステムやプログラム全体に関わるバグが潜んでいるかもしれません。ですから、可能な限り速やかに発見できるように、早い時期にテストするべきです。
逆に、重要なパターンのテストがうまくいったなら、ある程度の余裕を持って詳細項目をテストできます。

Structured Basic Test

さて、ではテストパターンを作成する具体的な指標について話をしましょう。
まず、テストパターンが少ないほうから(笑)
構造基本テストとでも訳しましょうか。このテスト方法は、制御の流れを検査する方法です。これによって求められるテストパターンは、最低でもこのくらいのテストはしなきゃいかんだろうというものです。
  1. まずルーチンの開始を1とします。
  2. if や while や for や and や or や…そういうのが出てくるたびに1を加えます。
  3. case文のそれぞれのcase毎に、1を加えます。
こうして求めたものが、構造基本検査パターン数になります。
をこちらに用意しました。

Data Flow Test

Structured Basic Testが制御フローに着目したテスト方法であるのに対して、Data Flow Testはデータフローに着目したテストです。

データ(広義の変数)には、大きく分けて3つの状態が知られています。
定義された 初期化されているが、まだ使われていない状態
使用された 計算に、ルーチンの引数や、ifでの判断などに使われたという状態
捨てられた 定義されたが、何らかの理由で未定義か不定になっている状態
たとえば、forのインデックスはforループを抜けたら「不定」です。
また、C言語でmallocされたもの、C++で newされたものは、それぞれfree、deleteされたあとは未定義になります。
この「データの状態」に、ルーチンに「入った」と、ルーチンから「出た」を組み合わせて、テストパターンを作成します。
以下のような組み合わせがおこったら、それだけで要注意です。
定義されて、定義された 通常は、定義されて使用されて捨てられるわけです。
2回初期化されていたりすると、それはちょっと妖しいかもしれません。
定義されて、出た 使いもしないのに定義してるってことですよね。これは変です。
不要なコードは読みにくく、バグを潜ませる元になります。
グローバル/モジュール変数の場合で、他のルーチンがそれを使うならば例外です
定義されて、捨てられた だから使えってば(^^;。
使わないデータならば、定義するべきではありませんよね。
入って、捨てられた なぜ、いきなり捨てる(^^;
ほかのルーチンからコピーして作って、消すはずのトコを忘れたりしてませんか?
グローバル/モジュール変数の場合で、他のルーチンがそれを使うならば例外です
入って、使用された 定義しましょう(^^;
変数の初期化を忘れて、パラメータに渡したり、ifで判断しているケースです。
グローバル/モジュール変数の場合で、他のルーチンがそれを使うならば例外です
捨てられて、捨てられた 捨てまくり。ボロボロですなこれは(笑)
たとえば、C/C++で一度 free/deleteしたものを、またfree/deleteしてしまったりするのがこれです。
致命的なバグを引き起こす場合も多いです。
使用されて、定義された 順番をまちがえていませんか?
もっとも、いい場合もあります。
「定義されて、使用されて、定義された」ならば問題はないわけです。
使用される前に定義されているかどうか、確認しましょう。
理想をいえば変数についてそれぞれこれを行うことになります。
Structured Basic Testに比べて、非常にパターン数が増えます。
先に、Structured Basic Testを行うべきでしょうし、テストの手間と、その効果を考慮し、どのくらいの重要度のデータについてこのData Flow Testを行うか、先輩やプロジェクトリーダーと話し合って決めるべきでしょう。

テストパターンの絞り込み ─ 同値分離

いいテストパターンは、入力データのほとんどをそのパターン一つでカバーできます。
2つのケースが、同じエラーを起こすのなら、そのうちのどちらかのケースのみテストすれば良いことになります。
if (Payment < MINIMAM_CHARGE) {
	.......
}
こういったコードに対して、何種類テストしても、Payment が MINIMAM_CHARGEより小さいかどうかの問題に集約されますよね。
これを「同値分離」といいます。この場合、テストパターンは2種類でいいわけです。

バグの予想

前述の方法のほかに、「このへんが妖しいな」というノリでバグを発見することがあります。
直観と経験─といってしまうのはあんまりですので、ちょっとその辺も分析してみましょう。
この場合は、あらかじめ「このへんにバグがありそうだ」と想定してテストパターンを作成するということでして、この「想定する」ということ自体にかなりのノウハウが詰め込まれています。

統計的に、バグが潜みそうな個所として:

があります。

「境界」というのは、要するに「一つ違い」のチェックです。
n < 5 であるべきif文が n <= 5と書かれていたり、 n+1 < 5だったりするのはよくある事です。

不正なデータとは、小さすぎるデータ、大きすぎるデータ、無効なデータ(種類が正しくない)、サイズが違うデータ、初期化されていないデータなどです。
たとえば、給料に負が設定されるケースなど、あるいは(言語によっては)配列数が32768の配列など─
不正なデータをどう扱うかは、仕様によります。
「ハングする」「落ちる」という仕様もあるかもしれません。

古いデータというのは、以前と現在で、データ構造に変更があった場合、過去のデータ構造で正しくうごくかどうかということです。
もちろん、「過去のデータでは動かない」という仕様もあるでしょうが、ついついテストパターンから外してしまいがちですので、意識してテストするようにしたほうがいいでしょう。

テスト自身のバグ

見落としがちですがテストパターン自体に誤りがあることがあります。
それを避けるには: というような努力が必要です。

プログラムを作成するのと同じように、テストもまた注意深くおこないましょう。

テストに王道はありません。地道な、そして執拗な検査が、バグの発見につながると、私は思っています。


はじめに ルーチン モジュール 変数名の力 変数使用法 制御構造 レイアウト コメント テスト デバッグ 謝辞 Top of Site

Copyright (c) 2000 Takao Tamura