Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

TheSchwartzのworkerをテストする

いま、MankiwというTheSchwartz/Gearmanのclient/worker/woker managerをいい感じにするモジュールを作っています。

これを作る過程で、TheSchwartzのworkerをテストする際に、こういう感じでやったらいいのかなーとやってみたので、ちょっと書いてみます。

workerのテストはなかなか書きにくいものです。ロジックは別のクラスにしておいて、workerからはそのクラスにパラメタを与えて呼び出すだけ、という感じにしておくのがテスタビリティ的にいいのだとは思いますが、まあ、実際ちゃんとTheSchwartz経由でテストしてみないことには安心できません。しかしその場合、TheSchwartzのworkerは、その性質上、非同期に動作するので、テストが面倒です。

そこで、いま作ってる上述のMankiwでは、以下のようにしました。

  1. Test::mysqldでTheSchwartzのジョブDBを作成
  2. worker managerを立ち上げ、テスト対象のworkerを動作するようにする
  3. テストスクリプトからは、対象のworkerを指定してjobをinsertする
  4. workerは、テストスクリプトの子プロセスとして動作するので、処理が終わったら親プロセスにシグナルを送信して終了を伝える
  5. workerの終了を感知したテストスクリプトが、結果を検証する

具体的にはこんな感じ。

まずは、

  • TheSchwartzのジョブDBを作成し、
  • woker managerを起動して、テスト対象のworkerを実行可能にする

ここでは、Test::mysqldとProc::Guradを使っているので、テストスクリプトの実行ごとにDBは作成/削除され、プロセスはテスト実行とともにきれいに後始末されます。

https://github.com/kentaro/perl-mankiw/blob/master/lib/Mankiw/Test.pm

sub setup_theschwartz {
    my $mysqld = Test::mysqld->new(
        my_cnf => {
            'skip-networking' => '',
        }
    ) or plan skip_all => $Test::mysqld::errstr;

    open my $fh, "< $theschwartz_schema";
    my $schema = do { local $/ = undef; <$fh> };
    close $fh;

    my $dsn = $mysqld->dsn(dbname => '');
    my $dbh = DBI->connect($dsn, 'root', '');
       $dbh->do($_) for split /;\s*/, $schema;

    my $theschwartz_mankiw_guard = proc_guard(
        scalar(which('perl')), $worker_manager,
        '-i', $mysqld->dsn(dbname => 'test_theschwartz'),
        '-u', 'root',
        '-f',
        $theschwartz_config_file,
    );

    ($mysqld, $theschwartz_mankiw_guard);
}

テストスクリプトの該当の箇所を抜粋。ここでは、以下のことを行っています。

  • ジョブの処理の結果を、File::Tempによるテンポラリなファイルに書き込むことで、テストスクリプトとworekerの間でやりとりしています。実際には、wokerはDBなりなんなりの値を書き換えるというようなことをやると思うので、それが正しく行われているかどうかをテストすることになるでしょう。
  • workerが処理を終了すると、テストスクリプトにUSR1シグナルを送信します。それによりテストスクリプトはworkerの完了を感知し、処理結果の検証を行います。
  • また、待ち合わせが無限ループしないよう、ALRMシグナルでタイムアウト処理を行います。

https://github.com/kentaro/perl-mankiw/blob/master/t/theschwartz_basic.t

subtest 'theschwartz besic test' => sub {
    my $waiting = 1;

    local $SIG{USR1} = sub {
        $waiting = 0;
        open $fh, "< $filename";
        my $result = do { local $/ = undef; <$fh> };
        close $fh;

        is $result, 1, 'job result of theschwartz worker';

        done_testing;
    };

    local $SIG{ALRM} = sub {
        plan skip_all => 'timeout to wait theschwartz worker';
    };

    my $client = Mankiw::TheSchwartz::Client->new(databases => [{ dsn => $dsn, user => 'root', pass => '' }]);
       $client->insert('Test::Mankiw::Worker::TheSchwartz' => {
           result    => 1,
           tmpfile   => $filename,
           owner_pid => $$,
       });

    alarm 10;
    1 while ($waiting);
    alarm 0;
};

ちなみに、workerはこんな感じ。この仕組みの検証用なので、特に意味のあることはやってません。

https://github.com/kentaro/perl-mankiw/blob/master/t/lib/Test/Mankiw/Worker/TheSchwartz.pm

package Test::Mankiw::Worker::TheSchwartz;
use strict;
use warnings;

use parent qw(Mankiw::TheSchwartz::Worker::Base);

sub work {
    my ($class, $job) = @_;
    open my $fh, '>' . $job->arg->{tmpfile};
    print $fh $job->arg->{result};
    close $fh;
    kill 'USR1', $job->arg->{owner_pid};
    $job->completed;
}

!!1;

細かいやりかたの差異はあるでしょうが、おおむねこんな感じでテストを書くと、TheSchwartzのローカルでのセットアップから、worker managerの起動、workerによる実際の処理まで、全部いい感じにかけていいんじゃないでしょうか。いま作っているMankiwを使うと、このあたりがいい感じなるつもりでいます。