Hatena::Groupdann

dann's blog このページをアンテナに追加 RSSフィード

Fork me on GitHub

2008-08-02

複雑なデータ構造に対するテストをする - Test::Deep編

| 複雑なデータ構造に対するテストをする - Test::Deep編 - dann's blog を含むブックマーク はてなブックマーク - 複雑なデータ構造に対するテストをする - Test::Deep編 - dann's blog 複雑なデータ構造に対するテストをする - Test::Deep編 - dann's blog のブックマークコメント

HashやArrayを含む複雑なデータ構造を比較しテストする事のできるモジュールがTest::Deepです。

例えば、Hashの構造を比較したい場合、以下のようにcmp_deeplyというメソッドで比較する事ができます。

use strict;
use warnings;
use Test::More qw(no_plan);
use Test::Deep;

my $got      = { title => 'Moose', body => 'cool' };
my $expected = { title => 'Moose', body => 'good' };
cmp_deeply( $got, $expected );

失敗時には、どのキーが異なっているのかが表示されるのでわかりやすいですね。

# Failed test at testdeep.t line 8.

# Compared $data->{"body"}

# got : 'cool'

# expect : 'good'

# Looks like you failed 1 test of 1.

testdeep...... Dubious, test returned 1 (wstat 256, 0x100)

Failed 1/1 subtests

Happy Testing!

複雑なデータ構造に対するテストをする - Test::Differences編

| 複雑なデータ構造に対するテストをする - Test::Differences編 - dann's blog を含むブックマーク はてなブックマーク - 複雑なデータ構造に対するテストをする - Test::Differences編 - dann's blog 複雑なデータ構造に対するテストをする - Test::Differences編 - dann's blog のブックマークコメント

Perlでは複雑なデータ構造に対するテストを行うモジュールの一つとして、Test::Differencesという二つのモジュールが存在します。

例えば、以下のように複数行に渡る文字列があったとしましょう。

use strict;
use warnings;
use Test::More qw(no_plan);
use Test::Differences;

my $expected = 'Mooose
is
really cool';

my $got = 'MOOOOOOOSE
is
really cooooooooooooooooooooooool';

eq_or_diff $got, $expected, 'Same string';

この場合に、Test::Differencesを使って、文字列が一致しているかどうかを比較すると、失敗したときにdiffが表示されます。

testdifferences......1/? 
#   Failed test 'Same string'
#   at testdifferences.t line 14.
# +---+-----------------------------------+-------------+
# | Ln|Got                                |Expected     |
# +---+-----------------------------------+-------------+
# *  1|MOOOOOOOSE                         |Mooose       *
# |  2|is                                 |is           |
# *  3|really cooooooooooooooooooooooool  |really cool  *
# +---+-----------------------------------+-------------+
# Looks like you failed 1 test of 1.

行毎に文字列の比較結果が表示されるので、どこに間違いがあるかが簡単に確認できます。

とはいっても、複数行のデータを比較するときなんかそんなないよ!と思われる方もいるかもしれません。そう思われた方は、是非、以下のドキュメントを読んでみてください。

石井さんの、「オブジェクトの文字列表現を活用しよう」

http://www.objectclub.jp/community/memorial/homepage3.nifty.com/masarl/article/junit/to-string.html

これは、テストコードのリーダビリティを高めるための重要なテクニックの一つです。テスト対象の粒度を大きくとることで、テストの意図を明確にする事ができる。また、テストコードのリーダビリティも高くなります。

オブジェクトの文字列表現を活用してテストのリーダビリティを高めつつ、かつTest::Differencesを組み合わせる事で、テスト失敗時の原因をわかりやすくすることができます。

では、Happy Testing !

# Test::Diffdiffのstyleもsupportされるみたいですね.

http://use.perl.org/~Ovid/journal/37083

2008-07-21

Devel::Coverでカバレッジテスト

| Devel::Coverでカバレッジテスト - dann's blog を含むブックマーク はてなブックマーク - Devel::Coverでカバレッジテスト - dann's blog Devel::Coverでカバレッジテスト - dann's blog のブックマークコメント

機能テストで機能仕様を満たしているかを確認するのにテストケースを書く場合、それらのテストをしていてもカバレッジが100%になっていなければ、何らかの漏れがあるか、使われていない古いコードが残っているかの可能性が高くなります。どちらのケースも後々になって問題が大きくなってきます。

特に製品規模が大きくなればなるほど、使われないコードが残存するというのは、割とよくあるケースで、使われないコードが増えだすと、どこに手をいれてよいのかがわかりにくくなってきます。こうなってくると、段々とコードを変更する事が大変になってきます。

テストケースがあったとしても、カバレッジが低ければ、どこを触ってもデグレードしてしまう危険があるからです。

特に企業で開発する場合、ずっと一人でメンテをするということはなく、開発メンバは変わっていきます。このような場合に、テストカバレッジが低いと大変です。

2-3人で開発をしていて、全体を全ての人が把握しきれているという状態が未来永劫続く環境があれば、カバレッジが低くても何とかなりますが、現実的にはそのような状況は存在しえません。長い目でみると、カバレッジを高く保つ事は、保守性を高め、結果プロダクト開発の生産性を高めることになります。

Perlでは、このカバレッジテストをするためのモジュールとして、Devel::Coverというモジュールが存在します。http-engineの例を見ると、以下のようなスクリプトでcoverageを、Devel::Coverを使って測定しています。

#!/bin/zsh
rm -rf cover_db
make realclean
perl Makefile.PL
HARNESS_PERL_SWITCHES=-MDevel::Cover=+ignore,inc,-coverage,statement,branch,condition,path,subroutine make test
cover
open cover_db/coverage.html

上記のスクリプトに少し手を加えて、cronで日付単位で結果を生成するようにして、indexを生成するスクリプトを作っておけば、日常的にカバレッジを確認できるようになりますね。

特に、開発の中盤から後半、またサービスのリリース後においては、テストカバレッジが高い状況を作り出しておくと、いざ開発メンバが変わったときでも安心して開発ができるのではないかと思います。

2008-07-12

SeleniumRCでTestSuiteを実行

| SeleniumRCでTestSuiteを実行 - dann's blog を含むブックマーク はてなブックマーク - SeleniumRCでTestSuiteを実行 - dann's blog SeleniumRCでTestSuiteを実行 - dann's blog のブックマークコメント

SeleniumRCを使うケースだと定期的にTestSuiteを実行させたいなぁっていうのがあるんじゃないかと思います。以下のようなスクリプトをCRONで1日単位でまわして朝見るってだけでも、ユースケースレベルのテストを確認するのには使えます。


#!/bin/sh
BROWSER=*safari
BASE_URL=http://www.google.co.jp
TEST_SUITE=/Users/dann/workdir/cattest/MyApp/t/acceptance.html
TEST_RESULT=/Users/dann/workdir/cattest/MyApp/t/acceptance_result.html
java -jar selenium-server.jar -htmlSuite "$BROWSER" "$BASE_URL" "$TEST_SUITE" "$TEST_RESULT" -timeout 60000

複数ブラウザでテストするのも上記のスクリプトをちょっと書き換えれば簡単にできますね。テストサーバーがあって、バックエンドでテストを実行させておきたいというのは割とよくあるケースで、上記のような簡単なスクリプトでも十分実用で使えます。

subverionからテストケースをcheckoutして、testsuiteをtestcaseから自動生成してから、上記のスクリプトでテストを実行させるなんて使い方がいいでしょうね。testsuiteの生成などについては、以前エントリを書いたので参考にしてみてください。

他にも、Selenium AESといった、似たようなことができるものもあるようなので、会社としてやりたい場合には、もうちょっと凝った仕組みを用意してもいいかもしれあないですね。

http://www.enjoyxstudy.com/selenium/autoexec/

SeleniumのGoog/Badから考えるアクセプタンステスト

| SeleniumのGoog/Badから考えるアクセプタンステスト - dann's blog を含むブックマーク はてなブックマーク - SeleniumのGoog/Badから考えるアクセプタンステスト - dann's blog SeleniumのGoog/Badから考えるアクセプタンステスト - dann's blog のブックマークコメント

Seleniumは使いこなすと非常に強力なテストツールですが、向き不向きもあり、テスト用途によって、どのようなテストをどのようなタイミングで実行すべきかを変えた方がよくあります。そこで、SeleniumのGood/Bad、Seleniumのテストの用途、Seleniumのテストを書くべきタイミング、注意点についてまとめました。

今、何のテストを書いているのかを意識し、それに応じてテスト方法とテスト計画を変える事は、アプリケーションをテストする上でとても役に立つのではないかと思います。

では、それぞれ見ていきましょう。

SeleniumのGood/Bad

SeleniumのGoogな点
  • Cross browser testingが簡単
    • browserの指定だけすれば同じTest Suiteで複数のブラウザをテストする事ができます。これはSeleniumを使う事の最大のメリットです。
  • テストケース作成のコストが低い
    • SeleniumIDEがあるために、テスト生成が極めて簡単でテスト作成のとてもコストが小さくなっています
SeleniumのBadな点
  • Selenium RC serverが不安定
    • 途中で落ちるCI用途では全然使えない
    • そもそもbrowserが落ちることも...
  • ブラウザを使ったテストはとても遅いので、テストに時間がかかりすぎる
    • コミットの度に動かすにはあまりに時間がかかりすぎる

このことから、早くコミットの度に何度も実行するようなテストケースを作るという用途では殆ど使えません。少しテストスイートが大きくなると、割と早い段階で問題に気づく事になります。


Seleniumのアクセプタンステストの用途

Good/Badな点を考慮すると、用途は大きく分けて二つになります

  • Cross Broserのためのテスト
  • ユースケース毎のシナリオ

ユースケース単位で書きたいものと、Cross Browser用のテストがしたい部分はテストの粒度もかなり変わるので、Test Suiteも分けたほうがよいです。

Seleniumでのアクセプタンステストを書き出すタイミング

Cross Browserテスト

これはUIの要素の構造が決まれば書き始めたほうがいいです。Cross Browser系のテストはデグレードしたときに、早い段階でSeleniumRCでテストした方が最終的なコストは低くなります。

ただ、この部分のテストについても基本はJavaScript側での単体テストを中心とすべき内容で、JSのモジュール全体の結合テストとしての役割のほうが強くなります。

この目的のテストでは、Selenium以上に適したツールは今のところありません。基本はこちらを重点的にテストする目的でSeleniumを使った方が幸せになれます

ユースケースのシナリオテスト

バックエンドのアプリケーションがあるシナリオで動くという条件があり、かつUIの「構造」がある程度固まってきたタイミングで書くのがよいです。UIの構造が固まるというのは、どういうタイミングかというと、画面遷移が決定し、UIの各要素に意味が割り振られ、各要素にidを割り振れる状態になったときです。

基本的には、Seleniumのテストを書くタイミングは、ユースケース単位でバックエンドの機能が完成し、UIの要素についても固まったタイミングでテストを書くのがよいということになります。

結合テストの一部としての役割が強くなります。基本的には、Seleniumの結合テストは最小限にして、バックエンドのService側のLogicの単体テストを充実させるべきです。

テストの実行時間も長いため、何度も繰り返し実行できるわけではないため、Seleniumでの部分の結合テストに力をいれるというのは、あまりおすすめはできません。特に異常系のシナリオのテストをこれで充実させるのは、あまりおすすめはできません。

ただ、ユースケース単位でシナリオが一通りカバーできるSeleniumベースの結合テストを用意するのは、デグレードを防ぐという点においては有益です。

Seleniumのアクセプタンステストを実行するタイミング

それぞれテストの粒度が違うので、テストを実行すべきタイミングも違ってきます。

Cross Browser Testing

こちらは頻度は若干高めで実行したほうがよいです。デグレードする範囲が大きくなると、後で対応させるのは割に面倒です。影響範囲が小さいうちにテストをしたほうが楽になります。

この部分についても、JavaScriptが中心のアプリケーションであれば、全体のTest Suiteの実行は、SeleniumRCで定期的に実行させて、結果を朝みるという運用でも十分なケースもあるとは思います。

ユースケースシナリオのテスト
  • ユースケースのシナリオの全体のテストは、CIによるテストタイミング(日に2回程度)で十分です
  • ユースケースシナリオ単体のテストは、SeleniumIDEをベースにコミット時に影響のあるシナリオを実行するのでよいと思います。この時にSeleniumRCを使うのはテスト時間もかかり若干大げさな解だと言えます。
  • 主な目的は一部機能がデグレードしていないかを確認することになります

Seleniumのテストケースを書くときの注意点

  • テスト対象のアプリケーションのHTMLにidを割り振ること
    • テスト対象の要素には必ずidを割り振ったほうがよいです。ブラウザ毎にDOMの形が異なるため、テスト対象の要素がXPathに依存すると、cross browser testingができなくなります
  • 要素のロードタイミングに注意すること
    • wait系コマンドを上手く使わないとテストがタイミングに依存したテストケースになってしまうからです。Ajax系のアプリケーションでは注意をする必要があります

まとめ

  • Cross Browser用の結合テストにはSeleniumは最適
  • ユースケースシナリオ単位のテストは1セットは用意するのはよい。ただ、あまり時間をかけるべきところではなく、基本は単体テストを充実させるべき。

# SeleniumIDEでさくさくテストの発展系のエントリも暇をみつけて書きたいですね。HTMLのTestCaseをSeleniumRCで扱うのもよいのですが、あれはあまりテストケースの保守性が高いとはいえないので

2008-07-06

SeleniumIDEでさくさくアクセプタンステスト

| SeleniumIDEでさくさくアクセプタンステスト - dann's blog を含むブックマーク はてなブックマーク - SeleniumIDEでさくさくアクセプタンステスト - dann's blog SeleniumIDEでさくさくアクセプタンステスト - dann's blog のブックマークコメント

ユーザー操作をシミュレートしてテストをしたいというニーズはあって、それに使えるツールとしてSeleniumIDEがあります。以下では、SeleniumIDEを使って、簡単にアクセプタンステストをする方法を説明します。

  • SeleniumIDEでテストケース記録
    • t/acceptance/ にTestCaseのHTMLを保存
  • TestSuiteの生成
    • gen_acceptance_testsuite.pl
  • 実行
    • ./acceptance.sh

gen_acceptance_testsuite.pl

#!/usr/bin/env perl
use strict;
use warnings;

use String::TT qw(tt strip);
use Path::Class qw(dir file);

my $TESTCASE_DIR   = 't/acceptance';
my $TESTSUITE_FILE = 't/acceptance.html';

main();

sub main {
    my $testcases = collect_testcases($TESTCASE_DIR);
    generate_test_suite( $testcases, $TESTSUITE_FILE );
}

sub collect_testcases {
    my $dir            = shift;
    my $testcase_files = [];
    dir($dir)->recurse(
        callback => sub {
            my $file = shift;
            return unless -f $file;
            push @{$testcase_files}, $file;
        }
    );
    return $testcase_files;
}

sub generate_test_suite {
    my $testcases           = shift;
    my $test_suite_file     = shift;
    my $test_suite_template = strip tt q{
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
  <title>Test Suite</title>
</head>
<body>
<table id="suiteTable" cellpadding="1" cellspacing="1" border="1" class="selenium"><tbody>
<tr><td><b>Test Suite</b></td></tr>
[% FOREACH t = testcases %]
<tr><td><a href="[% t.absolute %]">[% t.basename %]</a></td></tr>
[% END %]
</tbody></table>
</body>
</html>
};

    my $fh = file($test_suite_file)->openw;
    $fh->printflush($test_suite_template);
    $fh->close;
}

acceptance.sh

適宜変数は書き換えてください

#!/bin/sh
BASEURL=http://www.google.co.jp/
TEST_DIR=file:///Users/dann/workdir/cattest/MyApp/t
TESTSUITE=$TEST_DIR/acceptance.html
RESULT=$TEST_DIR/acceptance_result.html
firefox-bin -chrome "chrome://selenium-ide-testrunner/content/selenium/TestRunner.html?baseURL=$BASEURL&test=$TESTSUITE&auto=true&resultsUrl=$RESULT"

ブラウザベースのアクセプタンステストはEnd to Endのテストとして絶対に必要で、SeleniumIDEはそれにはとても向いているツールですね。

ただ、これだけだとContinuous Integration用には使えないので(自動化できないという点で)、SeleniumRCと併用して使うのがいいですね。

ただ、SeleniumRCは不安定なので、Continuous Integration用に使うのはいまいちなので、その辺はMechanize系との使い分けが必要なわけですが、それについてはまた別エントリで。

# 個人がサービスのアクセプタンステスト作るには、これくらいテストコストが低くないとダメなんじゃないかとは思ってます。アプリケーション規模とテストのバランスについても、エントリに書きたいなと。

2008-06-05

xUnitでの単体テスト

| xUnitでの単体テスト - dann's blog を含むブックマーク はてなブックマーク - xUnitでの単体テスト - dann's blog xUnitでの単体テスト - dann's blog のブックマークコメント

単体テストでxUnitを使う場合、クラスに対して1つのテストクラスを作るのが一般的です。例えば、Dogクラスがあれば、Dog::Testというクラスを作り、Dogの振る舞いに対してテストを書きます。

クラスの振る舞いごとに、テストメソッドを書いていきます。テストの意図をテストメソッド名にします。そのため、テストケースがそのクラスの振る舞いの仕様を表現することになります。

ですから、クラスの仕様を確認したいときは、そのクラスに対応するパッケージのテストを見ればいいという形になります。

クラスの振る舞いに対してテストをするというのが明確になるという点が、xUnit系のテスティングフレームワークのよい点の一つだと思っていて、個人的にTest::Classがいいんじゃないかなと思っています。

Test::Classでヘルパースクリプトを作らずにテストを実行する

| Test::Classでヘルパースクリプトを作らずにテストを実行する - dann's blog を含むブックマーク はてなブックマーク - Test::Classでヘルパースクリプトを作らずにテストを実行する - dann's blog Test::Classでヘルパースクリプトを作らずにテストを実行する - dann's blog のブックマークコメント

ベースクラス

package My::TestBase;
use strict;
use warnings;

use base 'Test::Class';

INIT { Test::Class->runtests }

1;

テストクラス

package My::Dog::Test;
use base qw/My::TestBase/;
use Test::More;

sub test_bark : Test(2) {
    is 'bow', "bowbow", 'dog barks';
    is 'bowbow', "bowbow", 'dog barks twice';
}

1;

上記のようにベースクラスを用意してあげると、テストのクラスでruntestsを毎回書く必要は無くなるので、ブートストラップ用のスクリプトはいらないかなぁと。

後は、prove -lv t/lib/My/Dog/Test.pm とすればよいと。

(perl的な配置だと、prove -lv t/01-dog.t とかのほうがあってるのかな。そこらはまだよくわかってないけれど、クラスに対するテストであることがわかるのがいいかなと。)

もしくは、以下のように必ずテストパッケージの下部でruntestsを書いておいてもブートストラップ用のスクリプトは不要そうです。

package My::AnotherDog::Test;
use base qw/Test::Class/;
use Test::More;

sub test_bark : Test(2) {
    is 'bow',    "bowbow", 'dog barks';
    is 'bowbow', "bowbow", 'dog barks twice';
}

__PACKAGE__->runtests;

1;