ソフトウエアのテストは、一般的に、次のフェーズからなります。
単体テストとは、UT、Unit Testを意味しますが、何をUnitと見なすかは、プロジェクトによって異なります。最小はメソッド単位(場合によってはその中のブロック内)で、さらにクラス単位、画面単位のいずれかになります。単体といってもある程度は結合した状態で行うことがあり、プロジェクトによって何を指すかは微妙に違います。画面の場合、1クラスから成っている場合もあれば、複数クラスから成っている場合もあります。
Webアプリケーションで1画面という場合、次の3つのテストが絡んでくるので、どのように区分けをするか注意が必要です。
1.初期表示前のページから渡されるパラメータやセッションその他の情報によって同じサーブレットもしくはJSPでも表示が異なってきます。
2.JavascriptJavascriptはそのページに付随して、ブラウザ上で実行されるので、1の初期表示の内容に関連します。
3.アクションボタンやリンクをクリックすることによって生じるアクションで、別の画面に遷移したり、同じ画面で検索結果や入力エラーを表示したりします。
画面単位の場合、1と3では別の画面と関連します。そのため単体といっても、画面同士の結合テストになりえます。1,2をその画面の単体テストとして、3を遷移先画面の1のテストとすることもできます。ただし、チェック漏れを防止するためにやはり3も含めることは必要です。そうするとテスト項目に重複が出ます。画面を単位にするとこの辺の切り分けが難しいものです。
また、実装として、コントローラであるServlet、ロジック実行のモデルやEJB、表示のJSP、さらにそれぞれが別のクラスを呼び出している場合は、もはや単体とは言いがたくなります。
メソッド単位の場合、privateメソッドは開発時のみ、その部分を切り出して、単体で実行してテストして完了とします。もしくは単にメソッドの長さを調節するために切り出したメソッドやアダプター的なメソッドで実装があまりない場合はコードレビューのみで終了します。
単体テストは、ホワイトボックステストとブラックボックステストからなります。
・ホワイトボックステスト 実装を知った状態で条件分岐を網羅 ・ステートメント網羅 ・分岐網羅 ・ブラックボックステスト 実装が見えない、インターフェースのみ見える状態でのテスト ・関数(メソッド)ベース 引数として渡せるものをすべて渡す。正常値、臨界値、異常値を渡す。 ・仕様ベース 仕様に基づいてテストする
ブラックボックス・テストの特徴 長所:ユーザの立場からのテストが可能 短所:内部仕様の特殊な処理のテストが困難 ホワイトボックス・テストの特徴 長所:内部仕様に基づく網羅的テストが可能 短所:外部仕様に規定されていながら、実現されていない部分のテストが漏れる。構造の間違いによる不具合は見つけられない
−−本来、仕様が完全ならホワイトボックステストとブラックボックステストの違いはないのでは?
仕様になくても念には念を入れてエラーチェックを行っている場合があります。そのテストは異常系で行う場合もあります。通常は、仕様書を作るのは実装者とは異なる場合があるので(本来実装した部分を仕様に反映させるべきですが)、別々になります。
ブラックボックステストの場合内部仕様がわからないため、A, B, Cという三つの要素があっってそれぞれ2パターンある場合、どの組み合わせで不具合が出るかわからないため、2*2*2=8パターンの試験が必要になってきます。内部仕様がわかっていると、A,B,Cそれぞれ独立して依存関係がないことがわかっているので2+2+2=6で済みます。とはいえ、しばしば実装者も依存関係を完全に把握していないため、6パターンで済むと思っていたものが、実はテストしていない組み合わせで問題が起きることがわかったりします。
・モジュール間の連携部分をテストする。
ポイントは連携がうまく行っているかどうかということ。モジュール間での値の受け渡し、同じリソースの共有等の観点から行います。連動の仕方には、メソッドの引数のように直接連携先に受け渡す場合もあれば、メモリや外部ファイル、DBなどの外の情報を書き換えて連動させる場合もあります。
モジュールは階層構造をなしているので、最下位のモジュールの結合から徐々に上げて最上位までの結合のレベルがあります。最小のモジュールは、関数・メソッドですが、開発の段階ではさらに小さいブロックでテストしてから組み立てていきます。
別システム間の結合も結合テストに入ります。一般にシステムを連動させて全体のシステムを確認するのはシステムテストだとされていますが、ここでは結合部分に焦点を絞って確認します。
・モジュール間を連携させてテストする。
結合度が小さくても、組み立てたものを連動させて動かします。
・マルチスレッド、マルチプロセスプログラミングの場合、スレッド間、プロセス間の結合テストも必要です。同じリソースに対する同期がきちんと取れているか確認する必要があるのです。
実はここが最も見落としやすく、また通常では出ないバグが、運用で負荷がかかったときに出るバグです。規模が大きくなると、同じリソースにアクセスしている部分を整理するのはかなり難しくなります。同時に更新したり、参照と同時に削除したりするケースを特に気をつけます。トランザクションの一貫性・隔離性にも配慮する必要があります。下手にロックをかけるとパフォーマンスが落ちるし、デッドロックにも注意する必要があるのです。0.00..秒の同時実行のために、高価な排他制御のコードを組み込む必要があり、致命的でない限り一旦エラー画面を表示して再度実行してもらう方がいいのではないかと思います。
ただ、マルチスレッドといっても、CPUを順に使いまわしているだけなので、基本変数のインクリメントなどは同期制御は不要です。
またユーザが異なるロールを持っている場合、そのユーザ間での連携が正しく行えるかも結合テストの対象とします。プロセス間、モジュール間の結合だけ見ていたのでは見落としていた部分を拾える場合もあります。
結合テストの目的は、あくまでもモジュールとモジュールの間の結合部分をテストすることです。きちんと接合しているか、共有リソースへのアクセスに問題はないかを重点を置いてテストすることが目的です。ところが、多くのプロジェクトや本でやっている意味をわからず、単体も結合もシステムテストも同じことをやっているのを多く見かけます。
あともう一点、他システムや他プロセスとの連動で注意したいのが、他システムはこちらの期待通りに動くとは限らないということです。他システムの障害に引きずられないように設計することが重要です。他システムが悪いと責任転嫁したところで、やはり引きずられて落ちてしまうと自分のところの作りこみが悪いということになります。
仕様に基づいて実環境と同等の状態で、仕様を満たしているかどうかテストします。結合テストでこれを行う場合も多いです。ただし、ここでは外部仕様に基づいてテストを行います。
・受入テスト内容的には、機能テストと同様になりますが、これは開発者ではなく、ユーザが製品を受け入れる目的で行います。
・運用テスト実運用と等しい状態でテストします。テストデータも実データとほぼ同等にします。
・性能テスト(パフォーマンステスト)負荷テストと違い、主に速度を問題にします。様々な負荷状態でテストします。一般には定常負荷時と最繁負荷時で測定します。
ただし工数管理のところでの述べましたが、システムテストの段階で性能を測定するのは遅い場合があります。DBのインデックスのチューニングなどソースに手を加えずにできるものはいいですが、テーブルの構造を変えたり、大幅にソースを改造することがないように、テーブル設計ができた段階で想定される量のデータを投入して代表的なSQLを発行してあらかじめ性能を確認するなどしておいた方が後戻りが少なくていいかもしれません。
・負荷テスト最大どこまでシステムが耐えられるかテストします。リクエスト数を上げていったり、DBに格納しているデータを極端に大きくしてみます。これは性能テストと同じ場合もありますが、こちらは限界性能を出すことが目的です。無限の負荷に耐える、あるいは性能を保証できるシステムなどありません。あらかじめユーザに限界を提示して、ユーザーにその範囲で使うようにしておくことが、いざ問題が起こったときのために大切です。
高負荷になったとき、システムは意外な様相を見せます。定常時には考えられなかったことも起きます。途端にレスポンスが悪化し、今までになかったエラーが出たりします。キュー溢れを起こしてリクエストをロストしたり、溜まっていたキューを一気に吐き出しそこで相手側を高負荷にしたりといった具合です。1プロセス、1スレッドで動いているシステムは、ある処理がボトルネックになって他の処理を止めてしまいます。DISK IOがネックになるときもあります。
・安定化テストこれは定常負荷、あるいは高負荷状態で、数日から数週間アプリケーションを起動したままにします。これは実際の運用のためのリハーサルで、24時間長期間稼動し続けられるかのテストです。実際同じだけの期間をテストすることはできないので、ある程度の長さで行います。それでも数日は必要なのでスケジュールを調整する必要があります。この試験でメモリリークやメモリへの不正アクセスなどを検出することができます。
・異常系テスト(耐障害テスト)ユーザの入力や操作ミス、DBやネットワーク、OS、他ハードのトラブルによる異常に対する反応を確認するテストです。このうちユーザの操作ミス等はここでの異常系には入れずに単体・結合・機能テストの段階で行うこともあり、それはプロジェクトによって異なります。
・初期状態テストこれは一般的には言われないものですが、わたしは非常に重要だと考えています。しばしばテストする際にはあらかじめデータを投入して行うため、データがなかったときの考慮が抜けていたりするものです。一番最初の段階ではデータがない状態からスタートします。その状態でも正しく動作するかどうかを確認します。
・その他そのほかにもインストールのテストやパッチ適用のテストも必要になります。インストール用のスクリプトが正しく動作するかどうかの確認をします。
また製品がきちんと動くかどうか簡単に確認するスモークテストもあります。これはインストールしてみて簡単な確認項目を実行してみて、製品がビルドに失敗していないか、バージョンが合っているか等を確認します。
またモンキーテストといって、でたらめにキーを叩いてみて異常を起こさないかを確認します。これは使い方のわかっていない人にやってもらうのがいいかもしれません。使い方を分かっていると無意識のうちに正しい使い方をしてしまうものです。しかし、初心者は全く想定していなかったような操作をするものです。
操作の順序を変えたり、二度押しをしたり、イレギュラーな入力をして見ることで、きちんとそれに対する対応ができているかを拾い出すこともできます。
テスター泣かせなのは、様々な条件がある中である条件を組み合わせたときに問題が出るというものです。テスト工数は限られているので、無限に可能ともいえる組み合わせをすべてテストするわけにはいきません。ある特定の文字を入力したらアプリケーションが落ちるなど、こんなものは通常のテストではまず拾うことはできません。にもかかわらずテストさえしっかりしていればバグはなくなるという幻想に取り付かれている人が多いのは残念です。
こういう点はテスターに任せるのではなく、プログラマーがしっかりチェックする必要があります。利用するライブラリに不具合がないかもきちんと情報を仕入れておく必要があります。
・セキュリティ最近はセキュリティが重視されていますが、今後はセキュリティテストなども出てくるかもしれません。今でもセキュリティの専門家がシステムに穴がないかをいろんな角度からチェックするサービスがあります。WebではクロスサイトスクリプティングやSQLインジェクション、パラメータの改ざん、Webサーバの脆弱性などいろいろな方法で穴をつくことができます。こういうテストも今後は必須になってくるでしょう。
JavaでのテストはJUnitを使って行うのが、最近は一般的になってきています。ただし何もかもできるわけではありません。
JUnitでできないもの
困難なもの
したがって、テスト仕様書で、JUnitでできるものとできないために手動で実施する必要があるものとを分けておいた方がいいでしょう。その意味でも、テスト仕様書は作った方がいいと思います。
仕様が明確でもロジックが複雑な場合、テストケースを作るのは結構難しいものです。内部のプログラムを見ても、なかなかアラは見つけにくいのです。以前、階層構造のデータを処理するプログラムを作ったとき、IDが後のものが前のものの親になるというパターンがなかったのでうまくいったように見えたのですが、実際、画面で更新してみるとうまくいかなくてそれに気づいたことがありました。その失敗をテストケースに反映させることはできましたが、明確にしづらかったです。if文の分岐は全部網羅されていたので、このカバレッジでは問題ないということになります。
仕様は単純に階層構造を自由に変えられるというものでしたが、どんなテストケースが考えられるのかは仕様からだと明確にできません。まあ優秀な方であれば、実装のロジックを見れば考慮漏れを発見できるかもしれませんが。これはテストケースを考え出すよりも、多くを実際に試してみた方がいいです。そうして発見できる問題もあり、発見したらそのテストケースをJUnitに追加するようにします。
ただし、なるべく自動化できるようにするために、モジュールを分割しておくことが重要です。特にViewとロジックを分離しておき、ViewはViewでテストでき、ロジックはロジックでテストできるようにすることです。ロジック同士の間も疎結合にしておけば、それぞれ別個にテストできます。
例えば、ロジックが、コントローラ−サービス−データアクセスオブジェクトの3層から構成されている場合、それぞれ別個にテストケースを作る場合と、全部まとめてコントローラに対してだけテストケースを作る場合とがあります。
後者にすると、複数のテストケースを作る必要がなくなります。またユーザに対する外部仕様をすべてそのテストケースの中で網羅させることができます。そして後段の構造を変えたとしてもテストケースを再作成する必要はありません。
前者の場合、それぞれの層でテストケースを作成することになりますが、その際テストケースはテスト対象のクラスで実装しているものだけでよいのです。もしさらに後段で起きるパターンもテスト対象にしようとするならテストケースは膨大になり、重複し、修正が厄介になります。
例えばサービス層で条件分岐が1つあるだけなら、それだけをテスト対象にすればいいのです。データアクセス層から返ってくる様々なデータパターンは考慮しません。そしてこのテストの時にはデータアクセス層はモックにして実際にDBにアクセスするようなことはしないようにします。
このように個別でテストするには、各層の間を疎結合にして、後段をモックへ切り替えられるようにしておきます。
テストを行う場合、単に引数を渡すだけではなく、ファイルやデータベースのデータなど、そのメソッドを呼び出すのに相応しい状況を作っておく必要があります。データベースにアクセスするのは重いので変わりにXMLファイルからダミーデータを読み出すようにする人もいますが私は推奨しません。サービス層のテストならいいですが、データアクセス層は実際にDBにアクセスして正しい結果が得られるかどうかのテストなのですから。方法としては、
1.テーブル作成から行う(最初にドロップする)。
2.毎回全部削除(truncate)して挿入する。
個々のテストケースで異なりますが、外部ファイルにしてローダーで挿入する。テーブル作成はスクリプトで行い、スクリプトバージョンを付け、使用バージョンをテストケース・ソースで指定しておきます。
3.毎回テストによって変化する一部を削除して挿入する。
マスターテーブルはそのまま使い、トランザクションテーブルも該当行だけを削除および挿入の対象とします。個々のケースで異なりますが、テストケース上にハードコードします。
が考えられます。これはできれば、オプションで切り替えられるようにするのが望ましいです。DB接続とユーザはプロパティファイルで切り替えられるようにします。接続定義は必ず全員共通のプロパティファイルから読み込むようにします。
DBへのデータのアンロード、ロードには結構時間がかかります。1テストメソッドごとに行うと全実行に莫大な時間がかかったりします。個々人の分担は少ないため時間は木にならないかもしれませんが、やはり最後は全体のテストケースを行いたいものです。
この場合、データセットは1つだけ用意しておいてうまくテストする順番を構成するか、あるいはテストメソッドの中で更新を行ったときに、テストメソッドの終了時に修復しておくようにすると、時間を短縮できます。ただしこの場合全体に共通のデータセットを作らなければならないので厄介です。テストデータ1箇所の変更が他のテストケースに影響を及ぼします。対処として標準のデータセットと専用のデータセットを用意し、専用のデータセットは入れたらすぐに消すようにするのがいいでしょう。
テストデータの中身は、ロジックに沿った抽象的なデータの方が望ましいです。例えば、メモ1, メモ2などのようなものです。その方がロジックの網羅性、正当性を検証しやすく、計算ロジックも単純にできるからです。ただマスターが固定されているものは具体的な方がいいです。ただし実データで使われる文字に近い方がいいでしょう。
前に苦労したものとして日付関係があります。現在日付を元に計算を行うロジックの場合、都度日付が変わってしまうのでテストケースとして成り立たなくなってしまいます。この際にはCalendarクラスのラッパーを作って任意の日時を設定できるようにしました。
例えば以下のメソッドをテストする場合を考えてみましょう。
int plus(int x, int y) { return x + y; }
テストケースとして常に3,4の組み合わせを選んだ場合、実際に実装が、
return 7;
となっていた場合、結果が正しいのでテストをOKとしてしまいがちです。やはり、結果が異なる、二つ以上のテストケースを選ぶべきでしょう。
テスト仕様書はできるだけ具体的、詳細に、だれが行っても同じ結果が得られるような記載にしておきます。項目は基本は1テスト1行で書きます。細かく一つ一つ書くと時間がかかるため、ある程度まとめて書くことになりますが、例えば「登録処理を行う」ではまとめすぎです。正常に登録が完了するケースと、ヴァリデーションで引っかかってエラーになる全てのケースは書く必要があります。
例えば、登録と更新で、バリデーションについて全く同じロジックを使っている場合、テストすべきでしょうか。ユーザ側の受け入れテストではブラックボックスのため必要ですが、開発側は必要ありません。実装がどうなっているか分からないときは、もしかしたらロジックは別々で組まれていたり、コピー&ペーストで行って、後で片方だけ修正して、修正が両方に反映されていないというケースがあるかもしれないのでテストが必要になります。
異常系のテストを行う場合、通常では起こらないケースなので、意図的に起こすのは難しいものです。そのために、ソースの中で、意図的に起こすように不正なデータを発生させたり、スリープさせたりします。
できれば、ソースをいじらないで実施したいものです。できる方法としては、データベースにアクセスしている部分があるなら、
・表にロックをかける
・トリガーを使って例外をthrowする
などの方法があります。ソースに手を加える場合は、そのソースは異常系テスト用というふうに分離し、マスターソースには手をつけないようにしましょう。
・徹底した入力チェック
外部に公開する場合、不正アクセスは付き物です。入力フォームにいろいろな値を入れてチェックします。
マルチスレッドの場合、同時に更新をかけると、排他制御を行っていないアプリケーションはすぐにデータの不整合が起きます。二つ画面を開いて同時に更新処理を実行してみたり、該当箇所が明らかな場合は、ソースに手を加えて、同時に処理が行われるとまずい箇所が、同時に実行されるようにしてテストします。
バグは基本的にすべて直すべきですが、納期との兼ね合いで、優先順位をつけて対応しなければならない状況もあります。発生の頻度、影響の度合い、対応の工数で判断します。そのエラーが再現性があるのか、あっても稀なのか、マニュアル以外の操作をした結果なのか、ほとんどありえない操作によって起きているのか等で判断します。ほとんどありえない操作によって引き起こされる問題に対応するのは得策ではありません。それによってデータ破壊などが起こらない限り、後回しにします。厄介なのは、再現性がはっきりしない障害ですが、原因究明には結構な時間を要します。