Skip to content.

Sections
Personal tools

Cotton Bolls: September 2004 バックナンバー

Document Actions

« July 2004 | トップページ | October 2004 »

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)