Skip to content.

Sections
Personal tools

Cotton Bolls

Document Actions

2004.10.31

RubyUnit実践講座―開発環境編

RubyのTest::Unitモジュールは,TestRunnerを全く意識しないでテストコードが書ける.例えばテストファイルfoo-test.rbがあって

#! ruby
require 'test/unit'

class FooTest < Test::Unit::TestCase
  def test_1
    assert_equal(2, 1+1)
  end
  def test_2
    assert_equal(1, 2-1)
  end
end

と書かれていれば

$ ruby foo-test.rb

とするだけで実行できてしまう.特定のテストメソッドだけ実行したいのなら--nameオプションを使って

$ ruby foo-test.rb --name=test_2

とするだけでいい.foo-test.rbにはTestRunnerがどこにもないのに動いてしまう.素晴らしい.

複数のテストファイルがあった場合はどうだろう.このときは,all-tests.rbを作ればよい:

#! ruby
require 'foo-test'
require 'bar-test'
require 'baz-test'

その後all-tests.rbを実行する:

$ ruby all-tests.rb

これだけだ.all-tests.rbを作るのが面倒なら,ワンライナーで実行することもできる.すなわち

$ echo '%w(foo-test bar-test baz-test).each { |f| require f }' | ruby

とすればいい(なぜか-eオプションを使う方法ができなくなっている).

ただ,こういった方法はテストファイルが増えてくると管理が面倒になってくる.そこで今回はGNU Makeを使ってテストを自動化する方法を紹介しよう.

一般にxUnitでは,次のテストが実行できれば十分だ:

  • プロジェクト全体のテスト
  • あるディレクトリ以下全体のテスト
  • あるテストファイルのテスト
  • あるテストファイル内のテストメソッドのテスト

そのためまずテストファイルのネーミングルールを決める.ここでは*-test.rbとしよう.Makeターゲットの仕様は次のようなものでいいだろう:

# プロジェクト全体を実行
$ make test all=true

# カレントディレクトリ以下全体を実行
$ make test

# テストファイルfoo-test.rbのみ実行
$ make foo.test

# foo-test.rb内のテストメソッド:test_2のみ実行
$ make foo.test method=test_2

さて,ディレクトリ階層にまたがったMakefileを作るため,common.mkの方法を使う.common.mkの方法とは,プロジェクトのルートディレクトリにcommon.mkを置き,各ディレクトリのMakefileからcommon.mkをインクルードする方法だ.こうすることでプロジェクト全体のMakefileを統一できる.

ルートディレクトリ直下のMakefileは

ROOT_DIR = .
include $(ROOT_DIR)/common.mk

と書き,ルートディレクトリから二階層下のMakefileでは

ROOT_DIR = ../..
include $(ROOT_DIR)/common.mk

と書く.ROOT_DIR変数にルートディレクトリへの相対パスを設定し,common.mkをインクルードするわけだ.

というわけで,ここからはcommon.mkの具体的な内容について書いていくことにしよう.ROOT_DIRにルートディレクトリのパスが設定されていることを覚えておいてほしい.

まずはrubyコマンドのオプションを設定しよう.ルートディレクトリにパスが通っていなければならないので,次のようになる:

ruby-flags = -Ks -I$(ROOT_DIR)
ruby-command = ruby $(ruby-flags)

-Ksオプションはソースの漢字コードがSJISであることを示すオプションだ.この辺は実際の開発に応じて変更してほしい.

まず簡単なターゲットから片付けていく.テストファイルまたはテストメソッド単体での実行だ:

%.test:
	$(ruby-command) $*-test.rb $(if $(method),--name=$(method))

$(if ...)ファンクションは,$(method)に値があるときだけ第二引数に展開される.
このターゲットでMakeを実行してみよう:

$ make foo.test
ruby -Ks -I. foo-test.rb 
Loaded suite foo-test
Started
..
Finished in 0.0 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

$ make foo.test method=test_2
ruby -Ks -I. foo-test.rb --name=test_2
Loaded suite foo-test
Started
.
Finished in 0.0 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

最初のコマンドがfoo-test.rbの実行で,次のコマンドがtest_2のメソッドを実行したことになる.

次にディレクトリ以下のテストファイルを一括で実行するためのターゲットを定義しよう.テストファイルをすべて列挙するには,findコマンドを使えばよい:

test-files = $(shell find . \
	-name '*-test.rb' \
	-and -not -path '*/CVS/*' \
	-print)

ここで,CVSを考慮してCVSディレクトリは除外した.このときディレクトリ以下をテストするターゲットは次のように書ける:

.PHONY:test
test:
	ruby -e '%w($(test-files)).each { |f| require f }'

プロジェクト全体のテストファイル実行はどうだろうか? 上のtest-files変数でfindコマンドのディレクトリ指定をROOT_DIRにすればいい:

all-test-files = $(shell find $(ROOT_DIR) \
	-name '*-test.rb' \
	-and -not -path '*/CVS/*' \
	-print)

さきほどと同じような変数定義になってしまったので,ファンクションを使おう.ここではfind-test-filesという名前にする:

find-test-files = $(shell find $1 \
	-name '*-test.rb' \
	-and -not -path '*/CVS/*' \
	-print)

このとき,

test-files = $(call find-test-files,.)

とすればカレントディレクトリ以下のテストファイルが設定されるし,

test-files = $(call find-test-files,$(ROOT_DIR))

とすればプロジェクト全体のテストファイルが設定される.プロジェクト全体のテストではall変数が定義されることを思い出せば

test-files = $(call find-test-files,$(if $(all),$(ROOT_DIR),.))

のようにすればいいことがわかる.

以上を踏まえて,common.mkの中身をすべて書いてみよう:

ruby-flags = -Ks -I$(ROOT_DIR)
ruby-command = ruby $(ruby-flags)

find-test-files = $(shell find $1 \
	-name '*-test.rb' \
	-and -not -path '*/CVS/*' \
	-print)

.PHONY:test
test:test-files = $(call find-test-files,$(if $(all),$(ROOT_DIR),.))
test:
	echo '%w($(test-files)).each { |f| require f }' | $(ruby-command)

%.test:
	$(ruby-command) $*-test.rb $(if $(method),--name=$(method))

想像以上に簡単ではないだろうか?
なお,実際にmakeコマンドでテストするときは,引数指定が面倒なのでEmacs Lispのコマンドを使っている(xunit.el).この辺の話はまた機会があれば紹介したいと思う.

| | コメント (1) | トラックバック (2)

2004.09.12

テストデータのセットアップ

Seasarのからさわぎ@大阪行ってきました.設計手法「くーす」の話.以下感想.

  • 一番最初にUIモック(画面プロトタイプ)を作れってのはいい.オブジェクト指向だと具体的な画面UIをなるべく排除するのがいいとされているが,そうするとユーザはついてこれない.
  • ロバストネス分析使えそう.でも用語はイヤ(バウンダリ,コントロール)
  • シナリオ分析かなりだるそう.やってられない.
  • 業務ロジックがStatelessということは,シナリオベースではなくメソッドベースのテストケースを書いてる?
  • 完璧なインターフェイス仕様を求めてるところはウォーターフォールっぽい?

以下,あんまり関係のないこととか

  • くーすって沖縄のお酒の名前?
  • きむきむさん=オレジュさん?
  • はぶさんのホワイトボードによるフォローうまい.ひがさんといいコンビですな.
  • 景品(Perl/CGI本)欲しいと思ったの僕ぐらい? 場違いPerlプログラマって僕だけ?(泣).

まあそんな感じ.くーすは機会があればぜひ使ってみようと思ったんですが,ただ受け入れられないことが....

自分Excelなんか嫌いですから!!! 残念!!!

ひがさんのVBA使ってくださいっていう言葉に頭くらくらしました(T_T).

というわけで,自分がくーす使うとしたらこんなふうにカスタマイズ.

  • Excelで書く部分はXML(+RubyでHTMLに出力)に変更
  • シナリオ分析はRubyスクリプトに変更
    (シーケンス図:RubyUnitテストケース,インターフェイス仕様:Rubyのクラス定義)

あるいはシナリオ分析はしょっていきなりJUnit+POJOでテストコード書き始めるかもしれないな.

さて,からさわぎでは説明がなかったが,SeasarではテストデータをExcelに準備してテストコードが書けるらしい.

僕の場合は,もちろんExcelは使わないし,別ファイルに書いたりもしない.テストデータは直接テストコードに書き込む派だ.ただ,テストデータをうまく埋め込まないと冗長でわかりにくいテストコードなってしまう.そこで,どんなふうにテストデータをセットアップしているか紹介しよう(但しPerlUnit).

まずDBテストを行うためのテストケースを定義する(SampleDBTestクラス):

package SampleDBTest;

use base qw(Test::Unit::TestCase);
use Test::Unit::Sql;

sub set_up {
    my $self = shift;
    $self->sql_set_up;
}

sub tear_down {
    my $self = shift;
    $self->sql_tear_down;
}
...

このクラスはTest::Unit::TestCaseの子クラスだが,Test::Unit::Sqlを宣言している(Test::Unit::Sqlは自作&非公開).このように書かれたテストケースはDBテスト用メソッドが使えるようになる(但し上のようにset_up, tear_downで個別にsql_set_upとsql_teardownメソッドを呼ぶ必要あり).

まず一番よく使うのがsave_tableメソッド:

my $employee = $self->save_table('employee');

save_tableメソッドはその名の通り,employeeテーブルを保護するメソッドだ.テスト側でemployeeテーブルをどういじってもマスターのテーブルには影響しない(DBのテスト参照).

また,このメソッドはTableオブジェクトを返す.Tableオブジェクトのinsertメソッドでレコードを追加していくことができる:

$employee->insert(emp_no => 1, dept_cd  => '10', name => '従業員1');
$employee->insert(emp_no => 2, dept_cd  => '10', name => '従業員2');

これでemployeeレコードを2件追加したことになる.名前つき引数でレコードのフィールド名と値を指定する.もしNULLを設定したいならundefでOKだ:

$employee->insert(emp_cd => 999, dept_cd => undef, name => '幽霊従業員');

このメソッドが使いやすいのは,明示的に指定のないフィールドにはダミー値が設定されることだ.これによってNOT NULL制約がたくさん書かれたテーブルでも簡単にテストデータを用意できる.

さらに,こんなこともできてしまう:

$employee->insert(emp_no => [10, 11, 12]);
$employee->insert(emp_no => [(20 .. 29)]);
$employee->insert(emp_no => [40, 41], dept_cd =>['40', '41']);

このコードでは,フィールド値に配列を指定している.こうすると配列が展開された件数分だけレコードが追加される.つまり,1行目はemp_noが10, 11, 12の3レコードを追加,2行目は20〜29までの10レコードを追加,3行目は各配列の直積,つまり(emp_no, dept_cd) = (40, '40'), (40, '41'), (41, '40'), (41, '41')の4レコードを追加する.集計データのテストを書くときに便利だ.

他に名前つき引数のいいところは,デフォルトパラメータを別に用意できることだ.具体的には次のようになる:

my %default_params = (emp_type => 1, update_time => '2004/09/12');
$employee->insert(emp_no => 100, %default_params);
$employee->insert(emp_no => 200, %default_params);
$employee->insert(emp_no => 300, %default_params, emp_type => 2);

こうするとemp_typeが1,update_timeが2004/09/12のデータをデフォルトで設定できる.但し,最後のemp_no:300のレコードはemp_type:2で上書きしている(後書き優先).Perlではこんなことも可能だ.これでテストデータはかなり簡略化できる.

以上で,save_tableメソッドの説明は終わりだが,この他にsave_load_tableメソッドがある.このメソッドは,CSVデータをレコードにインサートするメソッドだ:

$self->save_load_data(
company => "
01,会社1
02,会社2
03,会社3
"
dept => "
01,1a,部署1a
01,1b,部署1b
02,2a,部署2a
03,3a,部署3a
"
);

のコードでは,companyテーブルに3件のレコード,deptテーブルに4件のレコードをCSV形式でインサートする(Perlでは文字列中に改行しても大丈夫).上のコードは次と同じだ:

$self->save_data('company');
$self->save_data('dept');
$self->load_data(
company => "
01,会社1
02,会社2
03,会社3
"
dept => "
01,1a,部署1a
01,1b,部署1b
02,2a,部署2a
03,3a,部署3a
"
);

テーブルのカラム数が少ないマスタデータなどはsave_load_dataを利用すればよい.load_table, save_load_tableメソッドの問題点は,CSVにすべてのフィールド値を列挙しないといけないことだ.スキーマの仕様変更に弱いため注意する必要がある.

最後に,execute_sqlというメソッドがある.これはSQL文をそのまま発行するメソッドだ.

$self->execute_sql("
update employee set name = '新従業員名1' where emp_no = 1;
update employee set name = '新従業員名2' where emp_no = 2;
");

これまでのメソッドでカバーしきれない部分はこのメソッドを利用すればよい.

テストデータのセットアップコードを書くポイントは,特殊解をあらわにするメソッドを用意するということだ.これはJUnit実践講座にも書いた.テストに無関係な部分は極力テストコード上に出ないようにし,関係のあるデータだけ浮き上がってくるように心がける.そうすればわかりやすいテストコードが書けるようになるだろう.

| | コメント (4) | トラックバック (0)

2004.09.05

Observable関数

業務系のWebアプリには,よくマスター項目の入力支援サブウィンドウが必要になる.

例えば,郵便番号から都道府県と住所を補完してくれる入力支援サブウィンドウを考えよう(補完ボタン押下でサンプル表示):

都道府県 住所

ユーザが補完ボタンを押すと,住所補完サブウィンドウがポップアップする.住所補完サブウィンドウには,郵便番号マスタから検索された住所の候補が表示され,住所を選択するとサブウィンドウが閉じて都道府県と住所が補完される.

単純に実装すれば,住所補完サブウィンドウの選択ボタン押下時に次のようなJavaScriptコードを書けばよいだろう:

function selectButton_onClick(pref_code, address) {
   var form = opener.document.address_form;
   form.pref_code.value = pref_code;
   form.address.value = address;
   window.close();
}

ここで,親ウィンドウのHTMLが次のようになっていると仮定する:

<form action="..." name="address_form">
  ...
</form>

このとき,住所補完サブウィンドウからはopener.document.address_formが親ウィンドウに書いてあるformオブジェクトを表す.selectButton_onClick関数は,このformオブジェクト内にある都道府県リストボックスと住所テキストボックスの値を設定して自らのウィンドウを閉じる.

この実装がまずいのは明らかだ.住所補完サブウィンドウが親ウィンドウの詳細を知っている.そのため,カプセル化の原則を破っており,親ウィンドウの修正や住所補完サブウィンドウの再利用を困難にしてしまう.

これを改善するには,サブウィンドウから親ウィンドウへの依存関係を断ち切る必要がある.ここではObserverパターンを使って改善を試みよう.すなわち,住所補完サブウィンドウをObservable,親ウィンドウをObserverとみなせばよい.

最初の実装に戻って住所補完サブウィンドウの呼び出し部分を見てみよう:

function addressCompletionButton_onClick() {
   var zip_code = document.address_form.zip_code.value;
   var url = "address-completion.html?zip_code=" + zip_code;
   var style = "width=420,height=250";
   var subWindow = window.open(url, "AddressCompletion", style);
   subWindow.focus();
}

subWindowオブジェクトは住所補完サブウィンドウを表す.このウィンドウに対してaddObserverを呼び出すようにすればよい.素直に考えれば次のようになるだろう:

var subWindow = window.open(url, "AddressCompletion", style);
subWindow.addObserver(this);
subWindow.focus();

ところが,これはうまくいかない.住所補完サブウィンドウ(address-completion.html)で

function addObserver(observer) {
  ...
}

を定義しても,HTMLファイルが完全にロードされない限りsubWindow.addObserverが未定義になってしまうからだ.実際に調べてみたが,JavaScriptでこのロードタイミングを制御する方法はわからなかった.

そこで,JavaScript言語の動的な側面に着目しよう.次のようなObservable関数を用意する:

function Observable(o) {
    o.observers = new Array();
    o.addObserver = function(observer) {
        o.observers.push(observer);
    }
    o.notifyObservers = function(arg) {
        for (var i in o.observers) {
            o.observers[i](arg);
        }
    }
}

この関数は,任意のオブジェクトをObservableにしてしまう関数だ.

JavaScriptに慣れてない人はnotifyObserversメソッドの中身がよくわからないかもしれない.Observerパターンでは,Observerのupdateメソッドを呼ぶことでObservableの更新を通知する.ところが,JavaScriptは関数もオブジェクトだ.そのため,楽をしてupdateメソッドそのものをObserverとして利用している.

実際親ウィンドウ側でObservable関数を適用したコードは次のようになる:

var subWindow = window.open(url, "AddressCompletion", style);
Observable(subWindow);
var observer = function(arg) {
    var form = document.address_form;
    form.pref_code.value = arg.pref_code;
    form.address.value = arg.address;
}
subWindow.addObserver(observer);
subWindow.focus();

このように,都道府県コードと住所を更新する関数をObserverとして住所補完サブウィンドウに登録する.

住所補完サブウィンドウ側はすでにObservableになっているため,notifyObserversメソッドがそのまま利用できる:

function selectButton_onClick(pref_code, address) {
    notifyObservers({pref_code: pref_code, address: address});
    window.close();
}

notifyObserversの引数にはオブジェクトのハッシュを渡している(JavaScriptのオブジェクトイニシャライザ).オブジェクトイニシャライザは次とコードと等価だ:

var arg = new Object();
arg.pref_code = pref_code;
arg.address = address;
notifyObservers(arg);

このように,オブジェクトイニシャライザをPerlのような名前付き引数として利用すると便利だ.

以上で住所補完サブウィンドウから親ウィンドウの詳細を断ち切ることができた.逆に親ウィンドウは住所補完サブウィンドウのupdateの呼ばれ方を知る必要がある.オブジェクト指向の原則の一つ,「依存関係の逆転」だ.

ここで紹介したObservable関数が気に入ってるのは,たった10行程度の中にObserverパターンがすべて凝縮されていることにある.JavaScriptではそれが一つの関数になり,任意のオブジェクトに対して動的にMix-inできるところが面白い.他のパターンもこんなふうに簡潔に表現できるのかもしれない.

追記とお詫び
ここで書いたObservable関数の方法には問題があることがわかりました.

会社の同僚に指摘されたんですが,サブウィンドウ側でリロードするとnotifyObserversもobserversの情報も消えてしまいます.リロードには気がつきませんでした.そうすると,やはり親ウィンドウ側でObserverの管理クラスを用意しないとどうしようもなさそうです.これでは全然お手軽でもないので上記の方法は却下します.

あまり使い込んでないものを公表するのはよくないな….すみませんm(_ _)m.

| | コメント (2) | トラックバック (0)

2004.07.18

データベース定義XML

受託開発をしていると,Excelを使ってデータベース定義や固定長ファイルレイアウトを提供している開発がほとんどのようだ.だが,ポケットCVSリポジトリで書いたように,こういう情報こそテキストファイルにしてCVS上で管理したほうがよいと思う.

うちの開発では,たとえ外向きがExcelファイルでも内部ではXMLファイルを使って管理している.こういった情報は,プログラムのソースコード以上に重要で,スキーマ変更による影響がシステム全体に波及しかねない.また,スキーマ定義は開発中ころころ変わるのが当たり前だ.CVSでバージョン管理せずには対応できない.

例えば,社員マスタを次のように定義する(shain.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tables SYSTEM "tables.dtd">
<tables>
  <table name="shain" jname="社員マスタ">
    <field name="shain_cd" jname="社員コード" type="char" length="6" key="yes"/>
    <field name="shiten_cd" jname="支店コード" type="char" length="2"/>
    <field name="name" jname="名前" type="varchar" length="20">
      <desc>全角10文字</desc>
    </field>
    <field name="saishuu_koushin_ymd" jname="最終更新日" type="date"/>
  </table>
</tables>

他にも支店マスタ(shiten.xml)など,テーブルごとにXMLファイルを書いてバージョン管理する.

これらのXMLファイルが開発のハブとして働くことになる.つまり,これらを中心にいろいろなものを自動生成するわけだ.ざっと挙げれば次のようなものがある:

  • データベース定義仕様書
  • create-tables.sql(テーブル作成のsqlスクリプト)
  • insert, update, delete文のスケルトン作成
  • DAOクラスのスケルトン作成

ここで,最後のDAOクラスにはGeneration Gapパターンを使うこともある.

さて,上で挙げた対象ごとにrubyスクリプトを作り,Makefileにターゲットを書き込む.例えば,create-tables.sqlのターゲットは次のようになる:

create-tables.sql:*.xml
	ruby -Ks create-tables.rb create-tables.rsql *.xml > $@

ここで,create-tables.rbがcreate-table.sqlを作成するrubyスクリプト本体で,create-tables.rsqlがeRubyファイルだ.eRubyはjspに似た形式を持つ埋め込みrubyスクリプトで,rubyのライブラリの中では一番お世話になっている(ここを参照.作者の咳さんは来週XP祭りでしゃべるらしいです).

eRubyファイル:create-tables.rsqlの中身は次のようになる:

% for table in tables
CREATE TABLE <%= table.name %> (
%   for field in table.fields
  <%="%-21s %-14s %s" % [field.name, field.data_type, field.not_null] %>,
%   end
  PRIMARY KEY (<%= table.primary_keys.join(",") %>)
);
% end

sprintf演算子(%)を使って整形しているところはわかりにくいかもしれないが,eRubyをよく知らない人にもおおよそ理解できると思う.作るのは簡単だ.

他にinsert文の例も挙げておこう.Makefileは次の通り:

%.insert:
	ruby -Ks xml2insert.rb xml2insert.rsql $*.xml

このターゲットルールにより,社員マスタのinsert文が簡単に作れる:

$ make shain.insert
INSERT INTO shain
(shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES
('aaaaaa', 'bb', 'cccccccccccccccccccc', '2004-01-01');

eRubyファイル:xml2insert.rsqlは次の通り:

INSERT INTO <%= table.name %>
(<%= table.fields.collect { |f| f.name  }.join(", ") %>) VALUES
(<%= table.fields.collect { |f| f.value }.join(", ") %>);

このように,eRubyのおかげでrubyスクリプト本体はモデル(jspでいうところのJavaBean)を作るだけで済む.XMLファイルを読み込んでモデルを構築するだけだ.

XMLの読み込みにはREXMLを使っているが,Unicodeで処理されるためShift JISで書かれたXMLファイルを読むには少し工夫が必要だ.Excelファイルのこともあり,やはりShift JISで扱いたい.

このため,rexmlのメソッドをuconvでフックする.僕の場合,REXMLを直接使うのではなくrexml-sjis.rbという独自ライブラリを使っている:

require 'uconv'
require 'rexml/document'

module REXML
  class Attribute
    alias original_value value
    private :original_value
    def value
      Uconv.u8tosjis(original_value)
    end
  end
  class Element
    alias original_text text
    private :original_text
    def text
      Uconv.u8tosjis(original_text)
    end
  end
  class Text
    alias original_to_s to_s
    private :original_to_s
    def to_s
      Uconv.u8tosjis(original_to_s)
    end
  end
  class CData
    alias original_to_s to_s
    private :original_to_s
    def to_s
      Uconv.u8tosjis(original_to_s)
    end
  end
end

とりあえずこれでうまくいってるようだ.あ,それからcygwinのsetup.exeからインストールしたuconvモジュールは時々コアダンプしてしまうことがある.もしコアダンプするならよしだむさんのところから最新版uconvを再インストールすることも忘れないようにしよう.

ところで,スキーマ変更で一番大きなダメージを受けるのはテストコードだと思う.残念ながら,これについては十分な対策は見つかっていない.それでも,ほんの少しだけ修正が楽になる方法を紹介しておこう.

テーブルを扱ったテストケースでは,最初にinsert文でテーブルのレコードをセットアップすると思う.このとき,なるべくinsert文一発で行うのではなく,insert文とupdate文を組み合わせて行うほうがよい.つまり,次のようにレコード生成する:

INSERT INTO shain
(shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES
('1', 'bb', 'cccccccccccccccccccc', '2004-01-01');
INSERT INTO shain
(shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES
('2', 'bb', 'cccccccccccccccccccc', '2004-01-01');

UPDATE shain
SET
 shain_cd = '111111',
 name = '社員1'
WHERE
 shain_cd = '1'

UPDATE shain
SET
 shain_cd = '22222',
 name = '社員2'
WHERE
 shain_cd = '2'

最初のinsert文は自動生成されたinsert文で,主キーのみ一意にするよう変更する.その後テストに関係のある項目だけupdate文で更新しておく.

こうすると,たとえスキーマ変更でフィールドの追加や変更,削除があってもinsert文の修正だけで済むことが多い.気休めかもしれないが,大量にテストコードがあるとき変更が楽になるので,簡単なテーブル以外はこう書くよう心がけておいたほうがいいだろう.

| | コメント (0) | トラックバック (0)

2004.07.11

PerlUnitと動的スコープ

今回の仕事はPerlを使ったWebアプリの開発…って,なんとPerlだ.正直この歳(36)でPerlなんて夢にも思わなかった.

そもそも,最近はプロジェクトが変わるごとに開発言語も変わっている.curl,Java,C#と来て今度はPerlか….去年の11月から開発言語が4つも変わっている(泣).言語が変わる度に文法の勉強,開発環境の整備,パッケージの調査….いくらなんでもしんどいです.こんなんじゃプロジェクト工数かかるのも無理ないわな.

でも,この歳でまだプログラミング開発の仕事をやらせてもらってるのはありがたいと思う.昔から上流の仕事も管理職もまったく興味なし.そういう意味では今の会社に感謝している(…と一応持ち上げておく).

ところで,Perlは良くも悪くもバッドノウハウのかたまり言語だと思う.少し間違えただけですぐハマる.use strictや-wでもまだ足りない.他の言語では明らかにエラーになるところを余裕で素通りする.例えば,Error.pmのtry...catch.最後のセミコロンを忘れたら大変だ.動作がおかしくなるだけで文法エラーも何もでない:

try {
    $package->assert_str_equals($expected, $actual);
}
catch Test::Unit::Exception with {
    my $e = shift;
    $e->throw_new(-package => $package,
                  -file    => $filename,
                  -line    => $line,
                  -text    => $e->{-text});
}; # <= このセミコロンが重要!

他にハマッたのは親クラス宣言.親クラスは

use base qw(SuperClass);

と書くのが正解だが,編集中にうっかりして

use base SuperClass;

とやってしまった.こうするとUnknown Errorというエラーメッセージが出るだけだ.エラー箇所やヒントは一切ない(cygwin perl).一体なんなんだろう,この言語は….

その一方で,Perlはプリミティブであるがゆえに工夫次第でかなりのことができてしまう.これでPerlにのめりこむ人も多いのではないかな.

例えば,先ほどのtry...catch.Perlには例外処理なんてないのに,Error.pmというモジュールで実現できる.Exporterを使えばMixinも可能.なんとAspect.pmなんてモジュールもあるらしい(参考:Object Oriented Programming in Perl).

こういう機能は作る分にはすごく面白いと思う.けれども,利用する側はかなり不便だ.try..catchすら標準モジュールにないため,あるモジュールを利用するときは依存している他のモジュールを何個何個もインストールするはめになる.とにかく,いろんなモジュールが絡むため,再配布のことを考えるとかなり面倒.もし依存しているモジュールのバージョンが変わっていたら悲惨だろう.この辺Perlユーザはどう考えているのかな.

というわけで,今回もPerlのテスト環境や開発環境を整えることから始めることになった.特にこういう言語では,ユニットテストがしっかり書けないと泥沼になるのは目に見えているからだ.

まずはMutliTestRunner.pl.これは,複数のTestCaseを個別に実行したり,一度に実行したりできるようにするためのPerlスクリプトだ.これは,昔Javaで作ったMultiTestRunner(JUnit 実践講座 - GNU Make を使った開発環境の構築)のPerl版だ.時間がなかったのでテストメソッド単位の実行は省略した:

#!/usr/bin/perl -w

use strict;

use Test::Unit::TestSuite;
use Test::Unit::Debug qw(debug_pkgs);
use Test::Unit::TestRunner;

my $suite = Test::Unit::TestSuite->empty_new("A Test Suite");

foreach my $file (@ARGV) {
    my $package = Test::Unit::Loader::compile($file);
    my $test_case = Test::Unit::Loader::load_test_case($package);
    $suite->add_test($test_case) if $test_case;
}
my $testrunner = Test::Unit::TestRunner->new();
$testrunner->do_run($suite);

次にPerlUnit Converter.移植するのはかなり苦労したが,おおむね次のような形にまとめることができた:

package Test::Unit::ConvTest;
use strict;

use base qw(Test::Unit::TestCase);
use Test::Unit::Conv;

{
    package Test::Unit::ConvTest::TestBean;
    sub new {
        my $class = shift;
        my $self = {
            field1 => 'field1_val',
            field2 => 'field2_val',
        };
        return bless $self, $class;
    }
    sub get_field3 {
        return 'field3_val';
    }
}

sub TestBean {
    return __PACKAGE__ . "::TestBean";
}

sub test_bean_value_converter {
    my $self = shift;

    my $converter = DefaultConverter->new;
    $converter 
        = BeanValueConverter->new($converter, TestBean, "field1", "field2", "field3");
    my $expected = [
        "field1_val",
        "field2_val",
        "field3_val",
    ];
    $converter->assert_equals($expected, TestBean->new);
}

こんな感じでPerlの勉強をしながらテストケースを書いている.

さて,Perlには他の言語にはあまり見られない動的スコープ(Dynamic Scope)というものがある.これを使うとHello Worldのテストコードを簡単に書くことができる.すなわち,標準出力(STDOUT)を動的スコープでフックするわけだ:

{
    local *STDOUT;
    open(STDOUT, ">temp.out") || die "Can't redirect stdout";
    print "Hello World\n";
}

こうするとHello Worldの文字列はtemp.outファイルにリダイレクトされることになる.このことを利用して作ったのがStdoutCaptureクラスだ:

package Test::Unit::IO::StdoutCapture;
use strict;
use File::Temp qw(tempfile);
sub do(&) {
    my $block = shift;
    my ($f, $filename) = &tempfile();
    {
        local *STDOUT;
        open(STDOUT, ">$filename") || die "Can't redirect stdout";
        $block->();
    }
    open(TEMPFILE, "<$filename");
    my @out;
    while(<TEMPFILE>) {
        push @out, $_;
    }
    close(TEMPFILE);
    unlink($filename);
    return @out;
}
1;

使用例は以下の通り:

package Test::Unit::IO::StdoutCaptureTest;
use strict;

use base qw(Test::Unit::TestCase);

use Test::Unit::Conv;
use Test::Unit::IO::StdoutCapture;

sub test_basic {
    my $self = shift;
    my @out = Test::Unit::IO::StdoutCapture::do {
        print "line1\n";
        print STDOUT "line2\n";
        print "line3";
    };
    my $converter = DefaultConverter->new;
    my $expected = [ "line1\n", "line2\n", "line3" ];
    $converter->assert_equals($expected, \@out);
}
1;

このように,動的スコープでテストコードが楽に書けることがあるようだ.

ところで,Lispにも動的スコープがある.今回は,Emacs Lispでperldocコマンドを作っているときに役立った.

もともとperldocはPerl標準添付のシェルコマンドで,モジュールやサブルーチンの使い方を出力する.unixのmanコマンドのようなものだ.なかなかperlの関数が覚えられず,シェルで何度もperldocを動かしていたのでEmacsから呼べるperldocコマンドを作ることにした.

perldocコマンドはEmacsのmanコマンドみたいなもので十分だ.そこで,man.elのソースを見てみる.すると,manコマンドの本体はMan-getpage-in-background関数で,manual-program変数にシェルから起動するmanコマンド名を入れていることがすぐわかった.そこで,このmanual-program変数を動的スコープでフックする:

(defun perldoc (word)
  (interactive "sperldoc: ")
  (let ((manual-program))
    (setq manual-program "perldoc")
    (Man-getpage-in-background word)))

なんと,たったこれだけでperldocコマンドが動くようになった.普通ならもっとlispのコードを書かないといけないのにこれだけで済むとは驚いた.このように,動的スコープはグローバル変数みたいなものだからあまりお勧めできないが,ちょっとしたものなら役に立つ.PerlUnitでのうまい応用例も見つかればいいと思っている.

| | コメント (0) | トラックバック (0)