Mojo + DBIx::Skinny + Test::mysqld ことはじめ

※2010/8/21 モジュールを少し書き直した


テストはやっぱり同じプロダクトでやったほうがいいよ!と助言頂きましたのでTest::mysqld使うことにしました。
自分のやり方はいろいろ遠回りしてる気もしますが、一応自分のやりたいことはできたのでメモメモ。
ちなみにWAFは mojolicious でO/Rマッパは DBIx::Skinny とします。


ディレクトリ構成抜粋

-- lib
-- ...
-- db
`-- create_table.sql
-- t
-- fixture
`-- fixture.yaml
-- lib
-- MyApp
-- Test
-- mysqld.pm
`-- Test.pm

...

準備の流れ

  • create_table.sql/fixture.yaml を用意する
  • MyApp::Test::mysqld を作る
  • MyApp::Test を作る
  • Makefile に Test::mysqld を起動/停止する記述を追加

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!