404 motivation not found | t_ishidaのブログ

2月/08

13

テストの自動化

能書き

メモです。僕以外の誰が読んでも面白く有りません。

テストの自動化とは?

テストをする為のプログラムを書いて、それを実行して、プログラムにテストさせる事。

Perlの世界では非常に常識的な文化。

  • Perl
    • Test::More
    • Test::Simple
  • Java
    • JUnit
  • .NET
    • NUnit

自動化テストの種類

テストを自動化するメリット

  • 目検の漏れを少なくする事が出来る
    • 目検でやると、例えば、「AAA」 == 「AAA 」の違いが分かりにくい
    • 目検でやると、露骨に間違っているのに、見落とすかも知れない
    • 目検はそもそもしたくない
    • コンピュータはサボらない
  • 繰り返し行いやすい
    • 繰り返し同じテストをやる場合、人力だと時間がかかっても、コンピュータは、すぐ結果を出してくれる
    • 繰り返し行いやすいので、修正したら、その瞬間テストを実施できる。コンピュータが正しいと言っていれば、絶対正しいので、修正も入れやすい。
  • テスト仕様書は書きたくなくても、テストコードなら書きたい
    • 僕だけかも知れないけどExcelを開くよりは、テストコードは書くのが苦痛じゃない。
  • プログラマが見た場合、テストが有ると、関数やクラスの使い方が分かり易い。
    • どういう風に呼ぶの?
    • どういう値が返ってくるの?
    • どういう事しちゃいけないの?
    • ってのが一発で分かる
  • 手動ですべきテストを減らす事が出来る
    • 自動化する事が現実的なテストと、そうじゃないテストは勿論ある。

例えば、UI周りは自動化しづらい部分でもある。*1逆に、クラス・関数単位は修正も頻繁に入るし、テストは繰り返し行う必要がある。ここは自動化するのに向いていると言える。関数、クラス単位の品質が保障されているのであれば、手動で実施すべきテストの範囲を限定する事が出来る。

テストを自動化するデメリット

単純に開発の視点に立った場合はあまり無い。

が、プロジェクトと言う単位で考えると、いくつか

  • テスト仕様書の量が減ってしまう
    • 「テストしたの?」って監査に聞かれた時に「しました」、って言ってユニットテストを出す事は出来ない
    • エビデンスも出しにくいしね
  • テスト工数が減ってしまう
    • 時間清算のプロジェクトでは、儲けが減ってしまう。
  • ユニットテストに溺れる
    • ユニットテストをはじめに書いて、それが通ればOKと言う意識だけでいると、追加のテストを書かずに拡張してしまったり、テストケースそのものが間違っていたりと、色々アホな状況が起こったりする。

ユニットテストの手順

基本的にはテストファーストが好ましいとされている。が、自分でやる場合は、まとまった単位でプログラム作って、デバッグしてある程度動くようになったら、テストを書き始めると言う形をとっている。多分、脊髄反射でプログラムを書くタイプの人は後者になってしまう事が多いだろう。ただし、品質を高める視点に立った場合には、好ましくないと言われる。が、別にちゃんとテストするなら悪くは無いと、思っている。

  • 実プログラムの前に検査用のプログラムを書く
    • この時、正常引数、異常引数のパターンも書いて、関数の挙動として完全にテストされるようにする
  • プログラムを書き始める
  • プログラムを実行する

この繰り返し。

関数を作成する場合を基準に考えると分かり易い。*2テスティングフレームワークの類は後で勉強するとして、力技のユニットテストの実例を挙げておく。ちょうど自分的にタイムリーなので、例題はPHPで書くことにする。*3

//直接呼ばれたら、テストモード
if( str_replace( dirname(__FILE__),'' ,__FILE__ ) == $_SERVER['PHP_SELF'] ){
//正常系
print plus( 1  , 2 ) == 3    ? "正常系  is OK\n" : "regular is NG\n";
print plus( 'a', 2 ) == 'NG' ? "異常系  is OK\n" : "regular is NG\n";
}

function plus($a,$b){
if( !preg_match( '/^\d+$/',"$a$b" ) ) return 'NG';
return $a + $b;
}

つまり、テストファーストだと、テストモード(ifの中身)を先に書いてから、その全部がOKになるようにplusを実装するって事ね。この状況から、改修が起きた場合、例えば、第3引数が、trueの場合は、文字と、数字の足し算もやらせるよ。ってのが増えたとする。

//直接呼ばれたら、テストモード
if( str_replace( dirname(__FILE__),'' ,__FILE__ ) == $_SERVER['PHP_SELF'] ){
//正常系
//これもちゃんと動くの確認する。
print plus( 1  , 2 )       == 3    ? "正常系  is OK\n" : "regular is NG\n";
print plus( 'a', 2 )       == 'NG' ? "異常系  is OK\n" : "regular is NG\n";

//これが追加
print plus( 'a', 2, true ) == 'a2' ? "異常系  is OK\n" : "regular is NG\n";
}

function plus($a, $b, $c = false ){
if( !$c && !preg_match( '/^\d+$/', "$a$b" ) ) return 'NG';
return $c ? $a . $b : $a + $b;
}

と、と言うようにしていく。

DBが絡んだ場合のテストはどうしようか?と。これって戻り値とかで確認出来ないじゃん?と。そうしたら、例えば、データを追加する系の関数の場合は、多少面倒でも、実際に追加したデータをとってきてチェックする。面倒だよ・・って思うかも知れないけど、何度も追加された事を、目検する事に比べたら圧倒的に楽だし正確だ。

//可能なら全部消しておきたい
$db = DB::Connect($DSN);
$res = $db->query("delete from t_tbl);
$db->disconnect();

//呼び出す
addData(1,'test1','test11');
addData(2,'test2','test12');
addData(3,'test3','test13');

$db = DB::Connect($DSN);
//全部消せなかったのならば、主キーで必要な分だけとってくるようにしよう。
$res = $db->query("select * from t_tbl order by id");
$data = "";
while( $rec = $res->fetchRow() ) $data .= "$rec[0]\t$rec[1]\t$rec[2]\n"

$result = <<<DOCEND
1\ttest1\ttest11
2\ttest2\ttest12
3\ttest3\ttest13
DOCEND;
print $result == $data ? "addData is OK\n" : "addData is NG\n";

//別ファイルに有ると思って
function addData($x,$y,$z){
$db = DB::Connect($DSN);
$db->query("insert into t_tbl(id,name,comment) values( $x, '$y', '$z')");
$db->disconnect();
}

ファイルのテストはどうするか?ファイルを読み込んで、正しいかどうかをチェックするのが良いだろう。面倒だよ・・って思うかも知れないけど、何度もtailコマンド打つより絶対に楽だし正確だ。*4

基本的なメンタリティとしては、テスト用フレームワークを使ったとしても、この力技と一緒。比較のところがメソッドに変わるくらい。それから、データ構造が完璧に一致するか?のチェックとかを自動化したりしてくれる。*5上の例では文字列結合で期待値と一致するかどうか?をチェックしているけど、データ構造のまま比較できると言うこと。結局、ユニットテスト用のフレームワークを使ったとしても、凄く楽になる訳ではない。*6が、フレームワークを使うことによって、体裁が統一できるので、読む人が困りにくいと言うのが最大のメリットだろうか?

PHPのユニットテストについては、

http://www.ibm.com/developerworks/jp/opensource/library/os-php-unit/index.html

を参照する。

これについては調査不足なので書かない。後で調べて、後でまとめる。

まとめ

ああ、せっかく書いたので、うっかり読んじゃった人のために、

コツとか、結論とか、まとめだけ書いとくね。

コツ

  • 外部ファイルとかにテストデータとかが必要になるようなケースは、ユニットテストにテストデータ毎ぶちこんでおく。その場合は、文字コードにも留意すること。
  • テストデータをユニットテスト内部に持つ場合は、極力、そのテストデータから期待値も作るようにすること
  • テスト書くの面倒って思ったら、Excelで作ったテスト仕様書を印刷して、変更する度に泣きながらテストをしている自分を想像すること
  • それでも嫌だと思ったら、ここでサボると「納品後に泣きながらお客さんに謝って、上司に謝って徹夜でデバッグして、テストしないで再納品して、徹夜でデバッグして」の負のスパイラルに陥ることになると想像すること。
  • 謝った方が楽だなって思ったら、職を失って、橋の下で生活する羽目になって、DQN中学生にボコられて、血を吐きながら、シベリア寒気団に晒されている自分を想像すること。
  • これ、必須じゃないしケースバイケースだけどテスト用の環境設定もユニットテストにぶち込むと言うのも、相当便利*7だったりすると言うのを忘れないこと。

結論

新規につくるプログラムなら、なるべく書いておこう。
ただし、割とコストのかかる作業では有るので、
ケース別に考えておく。

使い捨てタイプの、自分しか使わないような類のプログラム
=> ユニットテスト不要

俺ライブラリの類
=> メンテの度に確認するのが面倒とかになるとアレなので書いておこう

人に使わせるライブラリの類
=> 絶対にメンテが必要になるので、時間の節約のために書いておこう。

お客さんに納品するものの類
=> 絶対にメンテが必要になるので、時間の節約のために書いておこう。
   また、いざと言うときのためにユニットテストからテスト仕様書を起こせるような、
   ハックを作っておければ尚可。(これしたいんだけど、なぜかまだやっていない。)

まず、ユニットテストのメリットを最大限に生かすには、

ユニットテストし易いようにプログラムを作る事が重要になる。

キーワードは疎結合と抽象化になる。

また、ユニットテストは割とオブジェクト指向と相性が良い。

*1:Webは返ってくるHTMLを期待値として比較する方法も無くは無いが現時的ではない

*2:クラスをテストする場合には、newして、それぞれのメソッドの戻り値や、プロパティの値を確認していくことになる

*3:力技の自動テストを作ったばっか

*4:例は省略

*5:PerlならTest::More::is_deeply

*6:楽になる訳ではないが、出来ることは増える。Perlならdies_okとか

*7:面倒な分だけ、本当に便利。ただし、諸刃の刃

Share and Enjoy:
  • Digg
  • del.icio.us
  • Google Bookmarks
  • Tumblr
  • email
  • Facebook
  • FriendFeed

RSS Feed

コメントはまだありません。

Leave a comment!

<< 懐かしいソース

EmacsのTips >>

Find it!

Theme Design by devolux.org

Tag Cloud