Gauche 用の単体テストフレームワーク GaUnit の0.1.4がリリースされました。
Gaucheには標準でgauche.testという単体テスト用のモジュールが付 属しています。このモジュールはテストスクリプトをはじめから順 に実行していくという素直なテスト実行方式を採用しています。こ の方式では一連のテストがどのように実行されていくかがわかりや すい半面、(場合によっては)以下のような問題があります。
また、gauche.testはテストの成功、失敗にかかわらずテスト結果が 常に冗長であるという問題があります。テストが失敗した項目(修正 の必要がある項目)についてのみ情報を詳細する方が、より合理的で しょう。
ということで、GaUnitです。GaUnitは xUnit系の単体テ ストフレームワークで、上記のgauche.testの問題を解決します。 また、テスト失敗時以外はテスト結果に余計な情報を出力しないため 無駄がありません。そんなGaUnitが今回のリリース(正確には1つ前の 0.1.3)からより書きやすいAPIを提供して、以下の2点を行うだけで すむようになりました。
実際のコードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(define-module test-your-library (extend test.unit.test-case) (use your-library)) (select-module test-your-module) (define (test-your-module-procedure1) (assert-equal "Good!" (your-module-procedure1)) ... #f) (define (test-your-module-procedure2) (assert-equal 29 (your-module-procedure2)) ... #f) (provide "test-your-module") |
最後にtest-*手続きの最後に#fを付けているのは末尾再帰の最適化 でバックトレースを落とさないようにするためです。
新しいAPIでは、普通のGaucheライブラリと同じようにテストを書 けます。テストのために覚えることと言えば、どのassert-*を使お うかということくらいです。これは、普段の別のライブラリを用い た開発と同じですね。
GaUnitでのテスト書き方をもっと知りたい人はチュートリアル を読んでください。
Firefox用アドオンやXULRunnerアプリケーションなどのいわゆるXULアプリケーションは、ロジック部を主にJavaScriptで記述するため、script.aculo.usのテスト関連機能などJavaScript用のテストツールを使って自動テストを行えます。しかし、一般的なJavaScript用のテストツールはWebアプリケーションをテストすることを主眼において開発されているため、利用できる機能に制限があったり、HTMLではなくXULを使用するXULアプリケーションのテストでは不具合が生じたりする場合があります。
UxU(UnitTest.XUL)は、著名なXULアプリケーション開発支援ツールであるMozLabをベースにクリアコードで開発を行っている自動テスト実行ツールです。FirefoxやThunderbirdなどのXULアプリケーション上での利用を前提としているため、前述のような制限や問題を気にすることなく自動テストを記述できる、便利なヘルパーメソッドを利用できる、などの特長があります。
テストの記述方法やヘルパーメソッドの一覧はUxUの紹介ページに情報がありますが、ここではFirefox用アドオンのXUL/Migemoのテストを実例として示しながら、UxUによる自動テストの方法について簡単にご紹介をしたいと思います。Subversionのリポジトリ内にテストのサンプル用に用意されたタグがありますので、まずはこちらから必要なファイル一式をチェックアウトしておいてください。
まずは最も簡単な例として、「tests」→「unit」とフォルダを辿った中にあるdocShellIterator.test.jsを見てみましょう。
XUL/MigemoはFirefoxのページ内検索を拡張するアドオンですので、フレーム内に検索語句が見つからなかったときは、子フレームや親フレームを検索対象として自動的に再検索を行うといった処理が必要になります。docShellIterator.test.jsでは、このための処理を担当するクラス「DocShellIterator」が正しく機能するかどうかをテストしています。
1 |
utils.include('../../components/pXMigemoFind.js', null, 'Shift_JIS'); |
冒頭では、ヘルパーメソッドのutils.includeを使って、DocShellIteratorクラスが定義されているファイルpXMigemoFind.jsの内容を取り込んでいます。第3引数で読み込むファイルのエンコーディングを指定していますが、これはファイルの中に含まれる日本語のコメントがShift_JISになっているためです。
なお、何らかの事情でそのままファイルをincludeできない(includeするとまずい)場合には、ファイルの内容をテキストとして読み込んで加工した後に評価するという方法もあります。上の例は、以下のように書き換えても同様に動作します。
1 2 3 4 5 |
var extract; eval('extract = function() { '+ utils.readFrom('../../components/pXMigemoFind.js') + '; return DocShellIterator }'); var DocShellIterator = extract(); |
utils.readFromはファイルの内容を文字列として返しますので、replaceなどを使って邪魔な部分を消してやれば、そのままではincludeできないファイルから必要な部分だけを取り出して評価できます。
このファイルにはテストケースが一つだけ定義されています。テストの前処理(setUp)と後処理(tearDown)は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 |
var DocShellIteratorTest = new TestCase('DocShellIteratorのユニットテスト'); DocShellIteratorTest.tests = { setUp : function() { yield Do(utils.loadURI('../res/frameTest.html')); }, tearDown : function() { iterator.destroy(); yield Do(utils.loadURI()); }, |
setUpとtearDownは、それぞれのテストを実行する前と後に毎回実行されます。このテストケースでは、テスト実行前に「テストに使用するフレームにHTMLファイルを読み込む」という処理を行い、テスト終了後に「フレームに空のページを読み込んで内容を破棄する」という処理を行うことで、毎回必ずクリーンなテスト環境を準備するようにしています。
setUpの中では、「フレームに指定したページを読み込み、読み込みが完了するのを待って次に進む」といった処理待ちを行うために、ヘルパーメソッドとyield式を使用しています。yieldは本来はJavaScript 1.7で導入されたジェネレータのための物ですが、UxUではこれを応用して処理待ちを実現しています。JavaScriptで処理待ちというと、タイマーやonloadのようなイベントハンドラを使う方法が真っ先に思い浮かぶと思いますが、yield式を使用すれば、それらの場合に比べて処理の流れをフラットに記述することができます。
個々のテストの定義を見てみましょう。以下のテストでは、DocShellIteratorクラスのインスタンスを生成して、検索対象のフレームを移動する処理を実際に行い、処理結果が期待されたものと等しいかどうかをテストしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
'前方検索': function() { // 1番目のフレームを初期状態としてインスタンス生成。 iterator = new DocShellIterator(content.frames[0], false); // 初期化は成功したか? assert.initialized(iterator, content.frames[0]); // 次のフレームに移動するメソッドを実行。 assert.isTrue(iterator.iterateNext()); // フォーカスは正常に2番目のフレームに移動したか? assert.focus(iterator, content.frames[1]); assert.isFalse(iterator.isInitial); // もう一度フレームを移動。 // 3番目のフレームは無いので、一巡して最上位のフレームにフォーカスする。 assert.isTrue(iterator.iterateNext()); // フォーカスは正常に最上位のフレームに移動したか? assert.focus(iterator, content); assert.isFalse(iterator.isInitial); // もう一度フレームを移動。1番目のフレームに戻る。 assert.isTrue(iterator.iterateNext()); // フォーカスは正常に1番目のフレームに移動したか? assert.focus(iterator, content.frames[0]); }, |
処理が成功したかどうかを確認する手続きを、アサーション(宣言)と呼びます。assert.isTrue、assert.isFalse、assert.equalsなどのアサーション用ヘルパーメソッドは、実行されると渡された値を評価します。実際に渡された値が期待された値と等しければそのまま処理を続行しますが、値が異なっていた場合は「アサーションに失敗した」という内容の例外を発生させてテストの実行が中断されます。これらの例外やメソッド実行時に発生した未知の例外はUxUのインターフェース上に逐次表示され、例外が発生した行のスタックトレースを後で辿ることができるため、どの時点で問題が起こったのか、どこまでは予想通りに正常に動いたのかを詳しく調べてデバッグに役立てられます。
UxUのページのアサーション一覧にないassert.initializedやassert.focusはこのテスト専用に定義したカスタムアサーションです。その実体は、このファイルの冒頭でassertオブジェクトに追加されている新しいメソッドで、以下のように、内部でより単純なアサーションを複数行っています。
1 2 3 4 5 6 7 8 9 |
assert.focus = function(aIterator, aFrame) { assert.equals(aFrame.location.href, aIterator.view.location.href); assert.equals(aFrame, aIterator.view); assert.equals(aFrame.document, aIterator.document); assert.equals(aFrame.document.body, aIterator.body); var docShell = getDocShellFromFrame(aFrame); assert.docShellEquals(docShell, aIterator.current); assert.isTrue(aIterator.isFindable); } |
複数の条件を満たしているかどうかを確認する必要がある場合、そのままテストを記述すると、同じコードがテストケースの中に大量に並んでしまいます。そういった一連の処理はカスタムアサーションとしてまとめておけば、テストケースの内容を綺麗に見やすくできます。
テストの実行は、MozRepl互換のUxUサーバを起動してコンソールから接続して行うか、MozUnit互換のテストランナーを起動して行います。「ツール」メニューから「UnitTest.XUL」→「MozUnitテストランナー」と辿り、テスト実行用のGUIを起動します。
UxUのテスト実行用GUIは、テストケースのファイルのドラッグ&ドロップを受け付けます。実行したいテストのファイル(docShellIterator.test.js)をウィンドウにドラッグ&ドロップすると「作業中のファイル」欄のファイルのパスがドロップされたファイルのものになりますので、後は「実行」ボタンを押すだけで自動テストを実行できます。
テストの現在の実行状況はプログレスメーターで表示されます。アサーションに失敗したり予期しないエラーが起こったりするとプログレスメーターが赤くなりますが、正常にテストが成功すれば緑色のままです。すべてのテストケースの実行結果が緑となることを目指して開発や修正を進めていきましょう。
このように、一連の処理と期待される結果をまとめておき、自動的にテストできるようにしておくと、開発した機能がきちんと動作しているかどうかを人手を煩わさず機械的に確かめられます。一通りのテストケースを作成するにはそれなりの手間と時間を要しますが、一度作成しておけばテストは何度でも簡単に実行できますので、うっかりミスによるエンバグを未然に防ぐ*1ことができます。
Firefox用のアドオンはFirefox自身が持っている機能と連携して動作するように作られることが多く、自動テストを作るのはなかなか大変です。UxUには処理待ち機能をはじめとした多くの便利な機能があり、「このような操作を行った時に、こういう結果になる」といった人間の操作をシミュレートする形の複雑なテストでも、処理の流れを追いやすい形で記述できます。Firefox用アドオンの自動テスト作成にはまさにうってつけと言えるでしょう。
*1 エンバグしてしまってもすぐにそれに気がつくことができる
LDAPのエントリを ActiveRecord風のAPIでア クセスするためのライブラリ、 ActiveLdap 1.0.1がリリースされました。
ActiveRecord風のAPIとは1エントリを1オブジェクトとして扱える ということです。例えば、ユーザの説明を変更する場合は以下のよ うになります。
1 2 3 |
alice = User.find("alice") alice.description = "New user" alice.save! |
ActiveRecordと同じように、各クラス間の関係を設定して便利にア クセスすることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class User < ActiveLdap::Base belongs_to :groups, :many => "memberUid" end class Group < ActiveLdap::Base has_many :users, :wrap => "memberUid" end alice = User.find("alice") alice.groups # => [Group("friend"), Group("office"), ...] alice.groups << Group.find("home") alice.groups # => [Group("friend"), Group("office"), Group("home"), ...] friend = Group.find("friend") friend.users # => [User("alice"), User("bob"), ...] |
ActiveRecordと同じように、Ruby on Railsと使用することもでき ます。
% script/plugin install http://ruby-activeldap.googlecode.com/svn/tags/r1.0.1/rails/plugin/active_ldap % script/generate scaffold_active_ldap % vim config/ldap.yml
ActiveLdapは以下のライブラリをバックエンドとして利用できます。
以下はActiveLdapに付属するベンチマークの結果です。ベンチマー クでは100エントリを検索しています。「Rehearsal(リハーサル)」 を行って、それぞれ2回ずつ実行しているのは、以前はキャッシュ などで2回目以降の結果がよくなることなどがあったためです。現 在はあまり意味がありませんが、歴史的に残っています。
% ruby benchmark/bench-al.rb --config benchmark/config.yaml Populating... Rehearsal ------------------------------------------------------- 1x: AL 0.080000 0.010000 0.090000 ( 0.098738) 1x: AL(No Obj) 0.010000 0.000000 0.010000 ( 0.016623) 1x: LDAP 0.000000 0.000000 0.000000 ( 0.008674) 1x: Net::LDAP 0.030000 0.000000 0.030000 ( 0.045199) ---------------------------------------------- total: 0.130000sec user system total real 1x: AL 0.080000 0.020000 0.100000 ( 0.100959) 1x: AL(No Obj) 0.010000 0.010000 0.020000 ( 0.020697) 1x: LDAP 0.000000 0.000000 0.000000 ( 0.010129) 1x: Net::LDAP 0.030000 0.000000 0.030000 ( 0.042075) Entries processed by Ruby/ActiveLdap: 100 Entries processed by Ruby/ActiveLdap (without object creation): 100 Entries processed by Ruby/LDAP: 100 Entries processed by Net::LDAP: 100 Cleaning...
各項目はそれぞれ以下の通りです。
上記の結果からは以下のことが言えます。
多くの場合、1度に100エントリを処理することは少ないでしょう。 そのため、通常はActiveLdapで各エントリをオブジェクト化しても 問題は少ないといえます。
もし、1度に多くのエントリを扱う場合で、読み込み専用ならば、 オブジェクト化しない方法で利用することでパフォーマンスを改善 することができます。
ActiveLdapを利用することでLDAPのエントリをオブジェクト指向的 なAPIで自然に処理することができます。
ActiveLdapは複数のLDAPバックエンドに対応しており、Rubyがイン ストールされている環境さえあれば動かすこともできます。 (Net::LDAPバックエンド使用時。ただしそんなに速くない)また、 JRubyでもほとんどの機能が動きます。
もし、Ruby/LDAPを利用できる環境であれば、Net::LDAPを直接利用 するよりも、ActiveLdap + Ruby/LDAPバックエンドを利用した方が よりオブジェクト指向らしいAPIでLDAPのエントリを操作できます。 また、速度が要求される場合であれば、オブジェクト化を行わない (オブジェクト指向らしいAPIを利用しない)ことにより、より高 速にLDAPのエントリを読み込むことができます。
Python用の単体テストフレームワークである Pikzie 0.9.2がリリースされま した。
以下のようにeasy_installでインストールできます。
% sudo easy_install Pikzie
Python用の単体テストフレームワークとしてはPythonに標準添付さ れている unittest や、unittestよりも柔軟にテストが書ける py.testなど があります。
また、unittest自体を拡張してより柔軟にテストが書けるようにし た nose もあります。noseはプラグイン方式をサポートしており、柔軟にテ ストが書ける機能以外にも、プラグインとして以下のような機能を 提供しています。
また、BDD用のテスティングフレームワークとしては pyspecがあります。
上記に挙げた既存の単体テストフレームワークには共通して以下の ような問題点があります*1。
上記の問題を解決することがPikzieを使うもっとも大きな理由にな ります。これは、テストの失敗結果を使いながらデバッグすること が多いからです。
テストを書くことの重要性、テストの書き方*2などを解説しているものはよくみますが、 テストが失敗してそれを修正していく過程を書いているものはなか なかみかけません。しかし、テストは一度成功したらそれ以降も成 功し続けるわけではないのです。開発の途中でテストは何度も何度 も失敗します。例えば、以下のような場合に既存のテストが失敗す るかもしれません。
つまり、新しいテスト・機能を開発していくときに既存のテストが 失敗することは当たり前のことです。
以下はunittestで書かれたテストです。
1 2 3 4 5 6 7 8 9 10 |
# unittest-test.py import unittest class FailTestCase(unittest.TestCase): def test_fail(self): x = 1110111011110 self.assertEquals(x + 100000, 1111111011111) if __name__ == '__main__': unittest.main() |
このテストの実行結果は以下のようになります。
% python unittest-test.py F ====================================================================== FAIL: test_fail (__main__.FailTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest-test.py", line 6, in test_fail self.assertEquals(x + 100000, 1111111011111) AssertionError: 1110111111110 != 1111111011111 ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
期待値と実測値は以下のように表示されています。
AssertionError: 1110111111110 != 1111111011111
どこが違うのかがわかりにくいのがわかると思います。
noseを用いた場合もテストの書き方は変わりません。ただし、テス トを実行するためにnosetestsコマンドを使う必要があります。
% nosetests unittest-test.py F ====================================================================== FAIL: test_fail (unittest-test.FailTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/unittest-test.py", line 6, in test_fail self.assertEquals(x + 100000, 1111111011111) AssertionError: 1110111111110 != 1111111011111 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
以下はpy.testで書いたテストです。
1 2 3 4 5 6 |
# pytest-test.py import py def test_fail(): x = 1110111011110 assert x + 100000 == 1111111011111 |
このテストの実行結果は以下のようになります。py.testコマンド で起動します。
% /tmp/py-0.9.1/py/bin/py.test pytest-test.py inserting into sys.path: /tmp/py-0.9.1 ============================= test process starts ============================= executable: /usr/bin/python (2.5.2-final-0) using py lib: /tmp/py-0.9.1/py <rev unknown> pytest-test.py[1] F _______________________________________________________________________________ ____________________________ entrypoint: test_fail ____________________________ def test_fail(): x = 1110111011110 E assert x + 100000 == 1111111011111 > assert (1110111011110 + 100000) == 1111111011111 [/tmp/pytest-test.py:5] _______________________________________________________________________________ ================== tests finished: 1 failed in 0.08 seconds ===================
unittestと違ってxの値が展開されていますが、
(1110111011110 + 100000)
の結果は表示されていません。こ
のため、実際の値が期待値とどう違うのかがわかりません。
最後にPikzieで書いたテストです。
1 2 3 4 5 6 7 |
# pikzie-test.py import pikzie class TestFail(pikzie.TestCase): def test_fail(self): x = 1110111011110 self.assert_equal(1111111011111, x + 100000) |
実行結果は以下の通りです。特別なテスト起動コマンドは必要あり ません。
% python pikzie-test.py F 1) Failure: TestFail.test_fail: self.assert_equal(1111111011111, x + 100000) pikzie-test.py:6: self.assert_equal(1111111011111, x + 100000) expected: <1111111011111> but was: <1110111111110> Finished in 0.005 seconds 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)
以下のように期待値と実測値が並べて表示されるので、違いをみつ けやすくなります。
expected: <1111111011111> but was: <1110111111110>
また、場合によってはdiffが表示されます。
1 2 3 4 5 6 |
# pikzie-test-diff.py import pikzie class TestDiff(pikzie.TestCase): def test_diff(self): self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa") |
実行結果です。
% python pikzie-test-diff.py F 1) Failure: TestDiff.test_diff: self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa") pikzie-test-diff.py:5: self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa") expected: <'aaaaaxaaaaaaaaa'> but was: <'aaaaaoaaaaaaaaa'> diff: - aaaaaxaaaaaaaaa ? ^ + aaaaaoaaaaaaaaa ? ^ Finished in 0.033 seconds 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)
また、長い行の場合は折り返してdiffを表示します。
1 2 3 4 5 6 7 8 9 10 11 |
# pikzie-test-diff-long import pikzie class TestDiffLong(pikzie.TestCase): def test_diff_long(self): self.assert_equal("ppppppppppppppppppppppyyyyyyyyyyyyyyy" "ttttttttttttttttttttttttttttttttttttt" "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon", "ppppppppppppppppppppppyyyyyyyyyyyyyyy" "ttttttttttttttttttttttttttttttttttttt" "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon") |
実行結果です。
% python pikzie-test-diff-long.py F 1) Failure: TestDiffLong.test_diff_long: "ppppppppppppppppppppppyyyyyyyyyyyyyyy" pikzie-test-diff-long.py:8: "ppppppppppppppppppppppyyyyyyyyyyyyyyy" expected: <'ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon'> but was: <'ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon'> diff: - ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon ? ^ + ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon ? ^ folded diff: ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhh - hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon ? - + hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon ? + Finished in 0.008 seconds 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)
diffの後に、長い行を折り返した結果に対するdiffも表示されてい ます。期待値・実測値が長い文字列で表現される場合は、折り返し た結果のdiffを見た方が異なる部分を見つけやすくなります。
テストは頻繁に失敗します。Pikzieはテストの修正に必要な情報を できるだけ多く、簡潔に表示します。これは、テストの修正を迅速 に行うために大事なことです。
ここでは書きませんでしたが、もちろん、Pikzieは他のテスティン グフレームワークと同じように柔軟にテストを書くことができます。
*1 unittestは命名規則がCamelCaseで PEP 8 -- Style Guide for Python Codeから外れ ているという問題もあります。
*2 例えば、テストの粒 度やテスト駆動開発など