Mojo + DBIx::Skinny + Test::mysqld ことはじめ
※2010/8/21 モジュールを少し書き直した
テストはやっぱり同じプロダクトでやったほうがいいよ!と助言頂きましたのでTest::mysqld使うことにしました。
自分のやり方はいろいろ遠回りしてる気もしますが、一応自分のやりたいことはできたのでメモメモ。
ちなみにWAFは mojolicious でO/Rマッパは DBIx::Skinny とします。
参考URL
http://mt.endeworks.jp/d-6/2009/10/things-ive-done-while-using-test-mysqld.html
http://www.art-code.org/presen/hokkaido.pm/1/index.html
こちらをミックスしたような方法。
ディレクトリ構成抜粋
-- lib | ||||
-- ... | ||||
-- db | ||||
`-- create_table.sql | ||||
-- t | ||||
-- fixture | ||||
`-- fixture.yaml | ||||
-- lib | ||||
-- MyApp | ||||
-- Test | ||||
-- mysqld.pm | ||||
`-- Test.pm |
...
準備の流れ
create_table.sql/fixture.yaml を用意する
テーブル定義とテストデータ定義を用意します。
fixtureにはTest::Fixture::DBIxSkinnyを利用します。
既にDBにテストデータが入っているときはTest::Fixture::DBIに同梱されている make_fixture_yaml.pl を使うとfixture.yamlが作れますよ!
ただし「schema:」がSkinnyのfixtureでは認識してくれないので「table:」に置換しておきましょう。
MyApp::Test::mysqld
ごめんなさい、そのまんまです><
http://mt.endeworks.jp/d-6/2009/10/things-ive-done-while-using-test-mysqld.html
package MyApp::Test::mysqld; use Moose; use namespace::clean -except => qw(meta); extends 'Test::mysqld'; around new => sub { my ($next, $class, %args) = @_; # By default, always skip networking if (! exists $args{my_cnf}->{skip_networking}) { $args{my_cnf}->{'skip-networking'} = ''; } # This is for us macports users. if you don't want to do this, set # MACPORTS=0 if ((exists $ENV{MACPORTS} ? $ENV{MACPORTS} : 1) && $^O eq 'darwin') { my $mysql_install_db = "/opt/local/bin/mysql_install_db5"; my $mysqld = "/opt/local/libexec/mysqld"; if ( -f $mysql_install_db && -x _ ) { $ENV{MYSQL_INSTALL_DB} ||= $mysql_install_db; } if ( -f $mysqld && -x _ ) { $ENV{MYSQLD} = $mysqld } } if ($ENV{MYSQL_INSTALL_DB}) { $args{mysql_install_db} ||= $ENV{MYSQL_INSTALL_DB}; } if ($ENV{MYSQLD}) { $args{mysqld} = $ENV{MYSQLD}; } return $next->($class, %args); }; __PACKAGE__->meta->make_immutable(inline_constructor => 0); 1;
MyApp::Test
Test::mysqldをほげほげするモジュール。
setup時にテーブル定義とテストデータを突っ込んでおきます。
package MyApp::Test; use Moose; has dsn => ( is => 'rw', isa => 'Str',); has db => ( is => 'rw', isa => 'Object',); has mysqld => ( is => 'rw', isa => 'Object',); __PACKAGE__->meta->make_immutable; no Moose; use Test::Fixture::DBIxSkinny; use MyApp::Test::mysqld; use MyApp::Model::DB; # skinny use SQL::SplitStatement; sub setup { my $self = shift; $self->create_mysqld(); my $db = MyApp::Model::DB->new({dsn => $self->dsn()}); $self->db($db); my $rows = $db->dbh->selectrow_arrayref( 'show tables', +{ Slice => +{} } ); # 既に初期化済みなら終了 return if $rows; my $sql = qq[ use test; create table initcheck(col int); ] . $self->sqlfile_to_str(); $self->init_database($db, $sql); # test data import construct_fixture( db => $db, fixture => "./t/fixture/fixture.yaml" ); } sub create_mysqld { my $self = shift; my $dsn = $ENV{TEST_DSN}; unless ($dsn) { my $mysqld = MyApp::Test::mysqld->new( +{ my_cnf => +{ 'skip-networking' => undef, }, } ) or die($Test::mysqld::errstr); $self->mysqld($mysqld); $dsn = $mysqld->dsn; } $self->dsn($dsn); } sub init_database { my $self = shift; my $db = shift; my $sql = shift; my $splitter = SQL::SplitStatement->new( keep_terminator => 1, keep_comments => 0, keep_empty_statement => 0, ); for ( $splitter->split($sql) ) { $db->do($_); } } sub sqlfile_to_str { my $self = shift; my $file = "../db/create_table.sql"; open my $fh, "<", $file or die $!." can't open file $file"; my $contents = do {local $/; <$fh>}; close $fh; return $contents; } # あとで説明 sub set_config_env { my $self = shift; my $conf_file = "./t/test_conf.yaml"; my $dsn = $self->db->{dsn}; open my $fh, ">", $conf_file; print $fh <<EOF; db: dsn: $dsn username: password: EOF close $fh; # コンフィグ切り替え $ENV{MYAPP_CONFIG} ||= $conf_file; }
Makefile.PL
ごめんなさい、これもそのまんまです><
http://mt.endeworks.jp/d-6/2009/10/things-ive-done-while-using-test-mysqld.html
この記述をしておくとmake実行時にテスト用mysqlが立ち上がって、make終了時に停止します。
use inc::Module::Install ..... # Fixup Makefile cause we like it! if (-f 'Makefile') { open (my $fh, '<', 'Makefile') or die "Could not open Makefile: $!"; my $makefile = do { local $/; <$fh> }; close $fh or die $!; $makefile =~ s/"-e" "(test_harness\(\$\(TEST_VERBOSE\), )/"-It\/lib" "-MMyApp::Test::mysqld" "-e" "\\\$\$SIG{INT} = sub { CORE::exit(1) }; my \\\$\$m = MyApp::Test::mysqld->new(); \\\$\$ENV{TEST_DSN} = \\\$\$m->dsn(); $1't\/lib', /; open (my $fh, '>', 'Makefile') or die "Could not open Makefile: $!"; print $fh $makefile; close $fh or die $!; }
テストする
ここまで来たら準備完了。
あとはテストを書き書きします。
#!/usr/bin/env perl use strict; use warnings; use Test::More; use Test::Mojo; use MyApp::Model::API; use lib "./t/lib"; use MyApp::Test; use MyApp::Test::mysqld; my $t = MyApp::Test->new(); $t->setup(); # テストで立ち上げたmysqlのコンフィグをファイルに書き出し、 # 環境変数に突っ込んでいる $t->set_config_env(); # コントローラのテスト { my $tm = Test::Mojo->new(app => 'MyApp'); $tm->get_ok("/hoge/fuga/piyo")->status_is(200)->content_like(qr/welcome!!/); } # モデルのテスト { my $rs = MyApp::Model::API->get_table1({}); #diag explain($res); is(...); # 普通にskinnyのクエリ投げたり $rs = $t->db->search('table1',{}); is(...); } done_testing;
ちょっと補足します。
今回のmojoアプリケーションではコンフィグファイルからDB情報を取得しているため、
引数で接続情報を渡すことは出来ません。
なので自前でコンフィグファイルを切り替えるような環境変数を用意してます。
MyApp.pmの抜粋
sub startup { my $self = shift; ... # config file my $conf = "config.yml"; $conf = $ENV{MYAPP_CONFIG} if $ENV{MYAPP_CONFIG}; $self->conf(Config::Any::YAML->load($conf)); ...
この機能を利用して、テスト用MySqlが立ち上がってたら
そのDSN情報をファイルに書き出し、
$ENV{MYAPP_CONFIG} にそのファイルのパスを埋め込んでいるのが
MyApp::Test::set_config_env です。
sub set_config_env { my $self = shift; my $conf_file = "./t/test_conf.yaml"; my $dsn = $self->db->{dsn}; open my $fh, ">", $conf_file; print $fh <<EOF; db: dsn: $dsn username: password: EOF close $fh; # コンフィグ切り替え $ENV{MYAPP_CONFIG} ||= $conf_file; }
これをやっておくと、テストスクリプト実行中のみ Test::mysqld のデータベースを見てくれるので
コントローラのテストもOK! やっほい
mojo の get でも使いたい
mojoのスクリプトにはgetという便利なコマンドがあるのですが、このときもテストDB使いたくなったり。
perl script/myapp get '/hoge/fuga/piyo'
コレと同じようなことをするスクリプトを用意しておけばOKです。
#!/usr/bin/env perl use strict; use warnings; use Test::More; use Test::Mojo; use lib "./t/lib"; use MyApp::Test; use MyApp::Test::mysqld; my $t = MyApp::Test->new(); $t->setup(); $t->set_config_env(); my $tm = Test::Mojo->new(app => 'MyApp'); my $res = $tm->get_ok($ARGV[0]); print $res->tx->res->body, "\n"; done_testing;
こんな感じで似たように使えます。
perl get_testdb.pl '/hoge/fuga/piyo'
yatta!