Perl Hackers Hub

第23回 Perlアプリケーションのテストと高速なCI環境構築術(1)

この記事を読むのに必要な時間:およそ 5 分

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回はmyfinderこと久森達郎さんで,テーマは「Perlアプリケーションのテストと高速なCI環境構築術」です。テストに利用するさまざまなモジュールから,CIContinuous Integration継続的インテグレーション)環境の構築,そして増えたテストを高速に実行する枠組みの構築までを紹介します。

なお本稿のサンプルコードは,本誌サポートサイトから入手できます。

テストの目的

まずはテストを行う目的を整理します。

コードを壊していないことを確認する

1人で開発するものであれば,どんな状況であれ責任を負うのは自分だけですが,チームや組織が複数にまたがる場合にはテストが重要になります。プロダクトが大きくなるにつれ,自分の開発によって想定外の機能にも影響を与えることが多くなります。逆にほかの開発者が行ったコミットによって自分の開発した部分に影響が出ることもあるので,これらを明確にするためにもテストは必要です。

実行・確認を自動化する

テストコードがないと,開発ごとに毎回手動で機能確認することになり,たいへん非効率です。非効率な状態を放置していると,確認漏れが起こったり,場合によっては十分にテストをせずにリリースしたりといった事態に発展しかねません。

自動化されていれば,誰でも気軽に実行でき,プログラムの改修が容易になるという副次的な効果もあります。

Perlアプリケーションのテスト

次に,Perlアプリケーションのテストについてまとめます。

Test::Moreによる基本的なテスト

Perlでテストを書くにあたって利用する最も一般的なモジュールは,Test::Moreでしょう。Perl 5.6.2からコアモジュールに入っているので,最近のPerl環境であればすぐ使い始めることができます。

Test::Moreはとてもシンプルなライブラリです。次のような単純な消費税率計算をするモジュールを例に説明します。

package ConsumptionTax::JP;
use parent qw/Class::Accessor::Fast/;

__PACKAGE__->mk_accessors(qw/ consumption_tax_rate /);

sub tax_include {
    my ($self, $price) = @_;
    return $price * (1 + $self->consumption_tax_rate);
}

1;

このモジュールのテストコードは次のようになります。

use Test::More;

use_ok("ConsumptionTax::JP");

my $consumption_jp
    = new ConsumptionTax::JP->new({
        consumption_tax_rate => 0.05,
    });

subtest " メソッド実装チェック" => sub {
    #tax_include という関数を実装しているか
    can_ok($consumption_jp, 'tax_include');
};

subtest "tax_include の動作チェック" => sub {
    my $price = 100;
    my $price_in_tax
        = $consumption_jp->tax_include($price);
    # 期待値と一致しているか
    is $price_in_tax, 105, 'match expected';
};

done_testing;

Test::Moreを使ってテストを書いた場合,テストコードの最後にdone_testing;を記述するのを忘れないようにしてください。このテストコードでは,

  • モジュールのuse
  • オブジェクトの生成
  • メソッドの実装有無
  • メソッドが期待した値を返すか

といった基本的な項目を網羅しています。

Test::Moreにはほかにも,

ok:真偽値のチェック

ok($val, "$val is true");

is_deeply:配列やハッシュの比較

is_deeply($val, { key => 'val'}, "$val is match");

like:正規表現との一致

like($val, qr/ 正規表現/, "$val is match");

といったメソッドを備えており,値やデータ構造の比較などもできるので,たいていの用途はカバーできるでしょう。

しかし実際のアプリケーションでは,Web APIなどの外部リソースに依存したコードや,時刻などテスト実行時に変化してしまうものも多くあります。そのようなテストも,ほかのテストモジュールを組み合わせて用いることでカバーできます。

Test::Stubによる外部依存テスト

Test::Stubは,外部サービスのAPIをコールしてその結果を利用するようなコードを書く場合などに役立つモジュールです。実際にはリクエストをせず,期待される結果に対してモジュールのテストを行うことができます。

たとえばTest::Stubを用いて,

use Test::More;
use Test::Stub qw/stub/;
use LWP::UserAgent;
use HTTP::Response;

my $ua = LWP::UserAgent->new;

subtest " レスポンス差し替え" => sub {
    stub($ua)->get(
        HTTP::Response->new(200, "OK")
    );
    is (
        $ua->get('http://example.com/')->code,
        200,
        'response code is ok'
    );
};

done_testing;

といったようにHTTP::Responseオブジェクトを返すだけの内容に差し替えることで,実際にHTTPリクエストを行わず結果オブジェクトを返すテストを書くことができます。

Test::MockTimeによる時刻のテスト

テストコードで時刻を扱うようなケースでは,単純にlocaltimeを用いると,実行タイミングによって取得できる時刻が異なるため,テストが通ったり失敗したりと再現性がない状態になってしまうことがあります。そういったときにはTest::MockTimeを用いることで,localtimeの挙動を書き換え,時間を固定してテストを実行できます。

use Test::More;
use Test::MockTime
    qw/set_absolute_time set_fixed_time/;

subtest " 時間指定" => sub {
    set_absolute_time(0);
    # set_absolute_time にsetした直後の時刻
    my $abs_time = time;
    sleep 1;
    # sleep から1 秒経過した直後の時刻
    is $abs_time + 1, time, '1 sec past';
};

subtest " 時間固定" => sub {
    set_fixed_time(0);
    # set_fixed_time にsetした時刻
    my $fixed_time = time;
    sleep 1;
    # set_fixed_time にsetした時刻
    is $fixed_time, time, 'fixed time';
};

done_testing;

このように,時間の経過や時間を固定したテストは,Test::MockTimeを活用することで比較的簡単に実装できます。

著者プロフィール

久森達郎(ひさもりたつろう)

株式会社フリークアウトの下回り系エンジニア。最近うっかり蒙古タンメン中本にハマってしまい,血が唐辛子色になりつつある。昨今のCasualムーブメントの一翼を担うMySQL Casualを運営。

YAPC::Asia 2012ではBest Talk Award 3位,YAPC::Asia 2013ではBest Talk Award 2位を受賞。

Twitter:@myfinder
blog:http://myfinder.hatenablog.com/

コメント

コメントの記入