11日目: ユニットテスト

今日は、完全に異なる内容: 自動化されたテストを語ります。このトピックの内容はとても大きいので、すべての内容をカバーするのに2日まるごとかかります。

Arkのすべてのテストは、慣習的にプロジェクトのt/ディレクトリに設置されます。

今日はユニットテストをカバーし、明日は機能テストに専念します。

ユニットテスト

ユニットテストを書くのはWeb開発のベストプラクティスの中で実行するのがもっとも難しいことです。 Web開発者は作品をテストすることに本当に慣れていないのと、たくさんの疑問がわき上がります: 機能を実装する前にテストを書かなければならないのか?何をテストする必要があるのか?テストはすべての単独の~エッジケース~をカバーする必要があるのか? すべてにおいてよいテストをできる方法は?しかし通常、最初のテストははるかに基本的です: どこで始めるのか?

私たちがテストを強く推奨しているとしても、Arkのアプローチは実践的です: テストを何もしないよりもしたほうが常によいです。テストなしのコードがすでにたくさんありますか?問題ありません。テストの利点から恩恵を受けるためにフルテストスイートを用意する必要はありません。コードでバグを見つけたときにテストを追加することから始めます。時間が経過して、あなたのコードはよりよいものになり、~コードカバレッジ~は上昇し、テストにより自信を持つようになります。実践的なアプローチを始めることで、 時間とともにテストがより快適になります。次のステップは新しい機能に対してテストを書くことです。すぐに、テストがやみつきになりますよ。

たいていのテストライブラリの問題は急激な学習曲線です。

Arkを使ったプロジェクトのテストには、CPAN上にある数多のTestモジュールのなかから好きな物を使う事ができます。

テストを書く作業を簡単にするためにArkがとてもシンプルなテストライブラリである Test::More を使用する事ができるのはそういうわけです。

前準備

テスト中に作られるデータが本番のデータベースに書き込まれてしまったら大問題です。 確実なテストを行うために、データベースの内容が適切に初期化された状態でテストがスタートする事を保証する必要もあります。

これらの問題を解決するために、テストの事前処理を行うための Jobeet::Test モジュールを作りましょう。 テスト用のデータベースにはSQLiteを使用します。

package Jobeet::Test;
use Ark 'Test';

use File::Temp qw/tempdir/;
use Jobeet::Models;

sub import {
    my ($class, $app, %options) = @_;
    $app ||= 'Jobeet';

    {
        my $dir = tempdir( CLEANUP => 1 );

        models('conf')->{database} = [
            "dbi:SQLite:$dir/jobeet-test-database.db", undef, undef,
            { sqlite_unicode => 1, ignore_version => 1 },
        ];
        models('Schema')->deploy;
    }

    @_ = ($class, $app, %options);
    goto $class->can('SUPER::import');
}

1;

テストの先頭で use Jobeet::Test; すると、テスト用の新しいデータベースが自動的に作成され、接続先に設定されます。 新しく作成されたテスト用のデータベースは、テストスクリプトが終了すると自動的に破棄されるので、本番のデータを壊す事無くテストを行う事ができるようになります。

初めてのテスト

早速、Jobeet::Test を使ってアプリケーションのテストを書いていきたいところですが、テストスクリプトが意図せずに本番のデータベースにつながってしまったら問題です。安心のために、まずは Jobeet::Test のテストを書く事にしましょう。 テストを書く事で Jobeet::Test がしっかりと動作している確証と、バグの無いコードを書いたという自信を得る事ができます。

ここで一つ注意しておく事があります。 今回は、Jobeet::Test を書いた後にテストを書きますが、本来なら先にテストを書いた後に実装をする -テスト駆動開発(TDD)- をすべき所です。

Test::More

テストには Test::More モジュールを使用します。 Test::More はとてもシンプルで扱いやすい、perlのテスト用モジュールです。 Test::More はいくつかのテスト用関数を提供します。 その中から特に良く使われる物をリストアップします:

メソッド 説明
ok($test) 条件をテストしてtrueであれば通る
is($value1, $value2) 2つの値を比較してそれらが等しい場合に通る
isnt($value, $value2) 2つの値を比較しそれが等しくない場合に通る
like($string, $regexp) 文字列を正規表現でテストする
unlike($string, $regexp) 文字列を正規表現にマッチしないことをチェックする
is_deeply($array1, $array2 2つの配列 or 連想配列が同じ値を持っていることをチェックする

jobeet_test.t

簡単なTest::More の説明が終わった所で、実際にテストを書いてみましょう。 Jobeet プロジェクトのテストは、共通して次のモジュールを使います。以降、省略する部分もあります。

use strict;
use warnings;
use Test::More;

use Jobeet::Test;
use Jobeet::Models;

Jobeet::Testuse された後に、設定ファイルのデータベース接続先がテスト様に作られた物になっているかを確かめます。 jobeet_test.tt/ ディレクトリ以下に作成し、以下のスクリプトを書き込みます。

use strict;
use warnings;
use Test::More tests => 1;

use Jobeet::Test;
use Jobeet::Models;

like models('conf')->{database}[0], qr{dbi:SQLite:/.+jobeet-test-database\.db}, 'connect mock database after "use Jobeet::Test"';

テストを実行する

テストを実行するには prove コマンドを使います。

prove コマンドは Test::Harness モジュールがインストールされていれば、既にあなたのマシンに入っています。

大抵の環境で、既にインストールされていると思いますが、インストールされていない場合は、一日目に行ったように

$ cpanm Test::Harness

を実行して、Test::Harness をインストールしましょう。

prove コマンドには、よく使われるいくつかのオプションがあります:

オプション 説明
-l 実行時にlibディレクトリをライブラリパスに加えます
-r テストを再帰的に実行しまう
-v 出力を冗長にします

それでは、先程書いた jobeet_test.t を実行してみましょう! Jobeetプロジェクトのrootディレクトリに移動して、prove -lr t/jobeet_test.tを実行します。

$ prove -l t/01_jobeet_test.t

t/jobeet_test.t .. ok
All tests successful.
Files=1, Tests=1,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.51 cusr  0.06 csys =  0.60 CPU)
Result: PASS

全てのテストが実行されて、緑色の文字があなたに全てのテストが pass した事を教えてくれます。

このテストで、Jobeet::Test モジュールを使用するとデータベースの接続先がテスト用のdbになっている事を確認できました。 これで安心してプロジェクトのテストを書いていく事ができます。

Schema クラスのテストを書く

テストを書くための下準備が終わったので、今までのチュートリアルに登場した順に沿ってテストを書いていく事にしましょう。

Jobeet::Schema::Result::Job

3日目に登場した Jobeet::Schema::Result::Job のテストから書いていきます。

基本的な所からテストを始める事が好ましいので、最初にデータの作成が正しく行われるかをテストすることにしましょう。

新しい category を作成して、それに付随する job を作ります。

新しくできた jobJobeet::Schema::Result::Job オブジェクトであるべきなので、先程 Test::More の説明で出て来た isa_ok を使用して確認します。 コードは次のようになります。

use strict;
use warnings;
use Test::More;

use Jobeet::Test;
use Jobeet::Models;

{
    my $new_category = models('Schema::Category')->create({ name => 'Programming' });
    my $new_job = $new_category->add_to_jobs({
        type         => 'full-time',
        company      => 'Sensio Labs',
        logo         => 'sensio-labs.gif',
        url          => 'http://www.sensiolabs.com/',
        position     => 'Web Developer',
        location     => 'Paris, France',
        description  => q[You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.],
        how_to_apply => 'Send your resume to fabien.potencier [at] sensio.com',
        is_public    => 1,
        is_activated => 1,
        token        => 'job_sensio_labs',
        email        => 'job@example.com',
    });

    isa_ok $new_job, 'Jobeet::Schema::Result::Job';
}

done_testing;

それでは実行してみましょう。

$ prove -l t/02_jobeet_schema_result_job.t

t/jobeet_schema_result_job.t .. ok
All tests successful.
Files=1, Tests=1,  2 wallclock secs ( 0.02 usr  0.01 sys +  0.55 cusr  0.07 csys =  0.65 CPU)
Result: PASS

全てのテストをPASSしました。まだテストは1つしかありませんが、All tests successfulの文字が誇らしいです。

次に、正しく作成される事が確認できた$new_jobに対して、その他のテストをしていきます。

3日目Jobeet::Schema::ResultBase に追加した、created_atupdated_at への日付の自動更新が、問題なく動作しているかのテストを追加しましょう。

isa_ok $new_job->created_at, 'DateTime';

isa_ok $new_job->updated_at, 'DateTime';

7日目Jobeet::Schema::Result::Job に追加した expires_atの日付の自動挿入も問題なく行われているかテストしましょう

isa_ok $new_job->expires_at, 'DateTime';

それでは、実行してみます。

$ prove -l t/02_jobeet_schema_result_job.t

t/jobeet_schema_result_job.t .. ok
All tests successful.
Files=1, Tests=4,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.55 cusr  0.07 csys =  0.65 CPU)
Result: PASS

おめでとうございます!! 全てのテストが通りました。 良い区切りなので、testsの数が 4 だということをTest::Moreに教えてあげます。

最終的に 02_jobeet_schema_result_job.t は次のようになりました。

use strict;
use warnings;
use Test::More tests => 4;

use Jobeet::Test;
use Jobeet::Models;

{
    my $new_category = models('Schema::Category')->create({ name => 'Programming' });
    my $new_job = $new_category->add_to_jobs({
        type         => 'full-time',
        company      => 'Sensio Labs',
        logo         => 'sensio-labs.gif',
        url          => 'http://www.sensiolabs.com/',
        position     => 'Web Developer',
        location     => 'Paris, France',
        description  => q[You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.],
        how_to_apply => 'Send your resume to fabien.potencier [at] sensio.com',
        is_public    => 1,
        is_activated => 1,
        token        => 'job_sensio_labs',
        email        => 'job@example.com',
    });

    isa_ok $new_job, 'Jobeet::Schema::Result::Job';

    isa_ok $new_job->created_at, 'DateTime';

    isa_ok $new_job->updated_at, 'DateTime';

    isa_ok $new_job->expires_at, 'DateTime';
}


done_testing;

Jobeet::Schema::Result::Category

続いてJobeet::Schema::Result::Categoryのテストも行います。

8日目slugカラムを追加し、更新される度に自動的に更新されるようにしたのを覚えているでしょうか? 幸いなことに、作業をした日からそれほど時間が経っていないので覚えていると思います。しかし、これが半年後だったらどうでしょうか?自信を持ってどのように自動更新されるかを説明できるでしょうか? あなたがアインシュタインもビックリの天才なら即答する事も可能かもしれませんが、大半の人はそうでないと思います。

あなた自身のために、そして将来このプロジェクトを引き継ぐかもしれない前途ある若者のために、テストを書くことにしましょう。 テストを書いておけば、DBIx::Class に明るくない若者が、不本意に Jobeet::Schema::Result::Categoryinsert , update メソッドを書き換えてしまったとしても、テストが通らなくなるので間違いに気付くようになります。 若者は喜び、Jobeetを利用しているユーザーはサービスが安定して動いてる事に満足を得てくれるでしょう。

さあ、テストを書いていきましょう。 01_jobeet_schema_result_category.t ファイルを作り、そこに Jobeet::Schema::Result::Category のテストを書いていきます。

まず始めに、insert 時に slug カラムが適切に作られているかをテストします。

use strict;
use warnings;
use Test::More;

use Jobeet::Test;
use Jobeet::Models;

{
    my $new_category = models('Schema::Category')->create({
        name => 'CamelCase',
    });

    is $new_category->slug, 'camel_case', 'slug column: create slug column automatically.';
}

done_testing;

実行してみます。

$ prove -lr t/03_jobeet_schema_result_category.t

t/03_jobeet_schema_result_category.t .. ok
All tests successful.
Files=1, Tests=1,  1 wallclock secs ( 0.03 usr  0.01 sys +  0.50 cusr  0.08 csys =  0.62 CPU)
Result: PASS

問題ないですね。

update 時に slug カラムが自動的に更新される動作のテストも追加します。

$new_category->name('UpdatedCategoryName');
$new_category->update;

is $new_category->slug, 'updated_category_name', 'slug column: update slug column automatically';

再度、実行してみます。

$ prove -lr t/jobeet_schema_result_category.t

t/jobeet_schema_result_category.t .. ok
All tests successful.
Files=1, Tests=2,  1 wallclock secs ( 0.03 usr  0.00 sys +  0.55 cusr  0.08 csys =  0.66 CPU)
Result: PASS

最後にテストの数をTest::Moreに教えてあげます。

最終的に 02_jobeet_schema_result_category.t のコードは、次のようになりました。

use strict;
use warnings;
use Test::More tests => 2;

use Jobeet::Test;
use Jobeet::Models;

{
    my $new_category = models('Schema::Category')->create({
        name => 'CamelCase',
    });

    is $new_category->slug, 'camel_case', 'slug column: create slug column automatically';

    $new_category->name('UpdatedCategoryName');
    $new_category->update;

    is $new_category->slug, 'updated_category_name', 'slug column: update slug column automatically';
}

done_testing;

続いて7日目で作成したJobeet::Schema::ResultSet::Categoryのテストを書いて行きます。

Jobeet::Schema::ResultSet::Category

04_jobeet_schema_resultset_category.tファイルを作り、Jobeet::Schema::ResultSet::Categoryのテストを書いていきます。

まずはテストに使うデータを作成します。

## create test data
my $programming_category =
    models('Schema::Category')->create({ name => 'Programming' });

my $programming_job = $programming_category->add_to_jobs({
    type         => 'full-time',
    company      => 'Sensio Labs',
    logo         => 'sensio-labs.gif',
    url          => 'http://www.sensiolabs.com/',
    position     => 'Web Developer',
    location     => 'Paris, France',
    description  => q[You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.],
    how_to_apply => 'Send your resume to fabien.potencier [at] sensio.com',
    is_public    => 1,
    is_activated => 1,
    token        => 'job_sensio_labs',
    email        => 'job@example.com',
});

get_with_jobs メソッドが Jobeet::Schema::ResultSet::Category を返すかのテストをします。返り値の型をチェックする事は有用です。

isa_ok models('Schema::Category')->get_with_jobs, 'Jobeet::Schema::ResultSet::Category', 'get_with_jobs: returning "Jobeet::Schema::ResultSet::Category" object';

get_with_jobs の返す ResultSet$programming_job が含まれている筈なので、そのテストも追加しましょう。

my $first_job = models('Schema::Category')->get_with_jobs->first;
is $first_job->id, $programming_job->id, 'get_with_jobs: got $programming_job';

7日目に実装した通り、get_with_jobsは少なくとも1つの利用可能な求人から全てのカテゴリを取得するメソッドです。 言い換えると、期限が切れているjobしかないカテゴリは習得しない、ということになります。 期待通りの動作をするかテストしましょう。

DateTime モジュールを使うのでコードの先頭に use DateTime; を追加してください。

my $yesterday = models('Schema')->now->subtract( days => 1 );

# Now, $programming_job expires
$programming_job->update({ expires_at => $yesterday });

is models('Schema::Category')->get_with_jobs->count, 0, 'get_with_jobs: expired job is not appeared';

テストを実行します。

prove -l t/04_jobeet_schema_resultset_category.t

t/jobeet_schema_resultset_category.t .. ok
All tests successful.
Files=1, Tests=3,  1 wallclock secs ( 0.02 usr  0.00 sys +  0.55 cusr  0.07 csys =  0.64 CPU)
Result: PASS

全てのテストが成功しました。 Jobeet::Schema::ResultSet::Category のテストは完了としましょう。

テストの数を Test::More に教えてあげます。 jobeet_schema_resultset_category.t の全コードは、次のようになりました。

use strict;
use warnings;
use Test::More tests => 3;

use Jobeet::Test;
use Jobeet::Models;
use DateTime;

{
    ## create test data
    my $programming_category =
        models('Schema::Category')->create({ name => 'Programming' });

    my $programming_job = $programming_category->add_to_jobs({
        type         => 'full-time',
        company      => 'Sensio Labs',
        logo         => 'sensio-labs.gif',
        url          => 'http://www.sensiolabs.com/',
        position     => 'Web Developer',
        location     => 'Paris, France',
        description  => q[You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.],
        how_to_apply => 'Send your resume to fabien.potencier [at] sensio.com',
        is_public    => 1,
        is_activated => 1,
        token        => 'job_sensio_labs',
        email        => 'job@example.com',
    });

    ## do test
    {
        isa_ok models('Schema::Category')->get_with_jobs, 'Jobeet::Schema::ResultSet::Category', 'get_with_jobs: returning "Jobeet::Schema::ResultSet::Category" object';
    }

    {
        my $first_job = models('Schema::Category')->get_with_jobs->first;

        is $first_job->id, $programming_job->id, 'get_with_jobs: got $programming_job';
    }

    {
        my $yesterday = models('Schema')->now->subtract( days => 1 );

        # Now, $programming_job expires
        $programming_job->update({ expires_at => $yesterday });

        is models('Schema::Category')->get_with_jobs->count, 0, 'get_with_jobs: expired job is not appeared';
    }
}

done_testing;

Jobeet::Schema::Result::Category 再び

7日目で、カテゴリResultオブジェクトにアクティブな求人を返す get_active_jobs メソッドを追加しました。

get_active_jobs に期待する振る舞いは次のようになります:

それでは上の項目が満たされているかのテストを、jobeet_schema_result_category.t に追加していきましょう。

まずはテスト用のデータを作ります。別ファイルにする方法もありますが、今回のようにユニットテストの場合はテスト用のデータがソース中に含まれている方が理解が容易なのでそうします。

$design_job_1 , $design_job_2 , $design_job_3 と3つの Design カテゴリーの job を作ります。

my $job_rs = models('Schema::Job');

## create test data
my $design_category = models('Schema::Category')->create({ name => 'Design' });

my $design_job_1 = $job_rs->create({
    category_id  => $design_category->id,
    company      => "Company 1",
    position     => 'Web Designer',
    location     => 'Paris, France',
    description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
    how_to_apply => "Send your resume to lorem.ipsum [at] company_1.sit",
    is_public    => 1,
    is_activated => 1,
    token        => "job_1",
    email        => 'job@example.com',
});

my $design_job_2 = $job_rs->create({
    category_id  => $design_category->id,
    company      => "Company 2",
    position     => 'Web Designer',
    location     => 'Paris, France',
    description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
    how_to_apply => "Send your resume to lorem.ipsum [at] company_2.sit",
    is_public    => 1,
    is_activated => 1,
    token        => "job_2",
    email        => 'job@example.com',
});

my $design_job_3 = $job_rs->create({
    category_id  => $design_category->id,
    company      => "Company 3",
    position     => 'Web Designer',
    location     => 'Paris, France',
    description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
    how_to_apply => "Send your resume to lorem.ipsum [at] company_3.sit",
    is_public    => 1,
    is_activated => 1,
    token        => "job_3",
    email        => 'job@example.com',
});

今回のテストから、説明文字列に日本語を使用する事にします。 Jobeetではutf-8でプログラムを書いているので、説明文字列に日本語を使用するために use utf8;binmode(Test::More->builder->$_, ':utf8') for qw/failure_output output todo_output/; をテストに追加します。

よってテストの上部は

use strict;
use warnings;
use utf8;
use Test::More tests => 2;

use Jobeet::Test;
use Jobeet::Models;

binmode(Test::More->builder->$_, ':utf8') for qw/failure_output output todo_output/;

になります。

それでは、日本語を使いつつ 期限切れでない job のみを返す の部分のテストをしましょう。

3つの job を登録したので、 $design_category->get_active_jobs->count は3を返す筈です。

is $design_category->get_active_jobs->count, 3, 'get_active_jobs: 3つ登録したjobの習得';

$design_job_2expires_at を昨日に設定すると期限切れになるので $design_category->get_active_jobs の返す結果セットには含まれなくなり $design_category->get_active_jobs->count は2を返す筈です。

my $yesterday = models('Schema')->now->subtract( days => 1 );
$design_job_2->update({ expires_at => $yesterday });

is $design_category->get_active_jobs->count, 2, 'get_active_jobs: $design_job_2 が期限切れになったので get_active_jobs の返り値に含まれなくなった';

次は、 作成日時(created_at)で降順にソートされている のテストです。

$design_job_1作成日時 を現在に、$design_job_3作成日時 を一時間前に設定します。 この状態で $design_category->get_active_jobs->first を実行すると、時間軸で後に作られた $design_job_1 が返ってくる筈です。

my $now = models('Schema')->now;
my $one_hour_ago = $now->clone->subtract( hours => 1);
my $tomorrow = $now->clone->add( days => 1 );

$design_job_1->update({ created_at  => $now });
$design_job_3->update({ created_at  => $one_hour_ago });

{
    my $first_job = $design_category->get_active_jobs->first;
    is $first_job->id, $design_job_1->id, 'get_active_jobs: $design_job_1が今、$design_job_3が1時間前に作られたので、$design_category->get_active_jobs->first は $design_job_1';
}

$design_job_3作成日時 を一日後にした場合は $design_category->get_active_jobs->first$design_job_3 を返す筈です。

$design_job_3->update({ created_at => $tomorrow });
{
    my $first_job = $design_category->get_active_jobs->first;
    is $first_job->id, $design_job_3->id, 'get_active_jobs: $design_job_3 が $design_job_1 より後に作られた事になったので、$design_category->get_active_jobs->first は $design_job_3';
}

最後に 第一引数で取得する行数を指定できる のテストをします。

{
    is $design_category->get_active_jobs->count, 2, 'get_active_jobs: $design_categoryのアクティブなjobは2つ';
    is $design_category->get_active_jobs({ rows => 1 })->count, 1, 'get_active_jobs: rowsパラメータで 1 を指定したので 1つだけ習得';
}

テスト数が増えたので、Test::More に数を教えてあげましょう。use Test::More tests => 8; に変更します。

最終的に jobeet_schema_result_category.t の全コードは次のようになりました。

use strict;
use warnings;
use utf8;
use Test::More tests => 8;

use Jobeet::Test;
use Jobeet::Models;
use DateTime;

binmode(Test::More->builder->$_, ':utf8') for qw/failure_output output todo_output/;

{
    my $new_category = models('Schema::Category')->create({
        name => 'CamelCase',
    });

    is $new_category->slug, 'camel_case', 'slug column: create slug column automatically';

    $new_category->name('UpdatedCategoryName');
    $new_category->update;

    is $new_category->slug, 'updated_category_name', 'slug column: update slug column automatically';
}


{
    my $job_rs = models('Schema::Job');

    ## create test data
    my $design_category = models('Schema::Category')->create({ name => 'Design' });

    my $design_job_1 = $job_rs->create({
        category_id  => $design_category->id,
        company      => "Company 1",
        position     => 'Web Designer',
        location     => 'Paris, France',
        description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
        how_to_apply => "Send your resume to lorem.ipsum [at] company_1.sit",
        is_public    => 1,
        is_activated => 1,
        token        => "job_1",
        email        => 'job@example.com',
    });

    my $design_job_2 = $job_rs->create({
        category_id  => $design_category->id,
        company      => "Company 2",
        position     => 'Web Designer',
        location     => 'Paris, France',
        description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
        how_to_apply => "Send your resume to lorem.ipsum [at] company_2.sit",
        is_public    => 1,
        is_activated => 1,
        token        => "job_2",
        email        => 'job@example.com',
    });

    my $design_job_3 = $job_rs->create({
        category_id  => $design_category->id,
        company      => "Company 3",
        position     => 'Web Designer',
        location     => 'Paris, France',
        description  => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
        how_to_apply => "Send your resume to lorem.ipsum [at] company_3.sit",
        is_public    => 1,
        is_activated => 1,
        token        => "job_3",
        email        => 'job@example.com',
    });

    ## do testing
    is $design_category->get_active_jobs->count, 3, 'get_active_jobs: 3つ登録したjobの習得';

    {
        my $yesterday = models('Schema')->now->subtract( days => 1 );
        $design_job_2->update({ expires_at => $yesterday });

        is $design_category->get_active_jobs->count, 2, 'get_active_jobs: $design_job_2 が期限切れになったので get_active_jobs の返り値に含まれなくなった';
    }

    {
        my $now = models('Schema')->now;
        my $one_hour_ago = $now->clone->subtract( hours => 1);
        my $tomorrow = $now->clone->add( days => 1 );

        $design_job_1->update({ created_at  => $now });
        $design_job_3->update({ created_at  => $one_hour_ago });

        {
            my $first_job = $design_category->get_active_jobs->first;
            is $first_job->id, $design_job_1->id, 'get_active_jobs: $design_job_1が今、$design_job_3が1時間前に作られたので、$design_category->get_active_jobs->first は $design_job_1';
        }

        $design_job_3->update({ created_at => $tomorrow });

        {
            my $first_job = $design_category->get_active_jobs->first;
            is $first_job->id, $design_job_3->id, 'get_active_jobs: $design_job_3 が $design_job_1 より後に作られた事になったので、$design_category->get_active_jobs->first は $design_job_3';
        }

    }

    {
        is $design_category->get_active_jobs->count, 2, 'get_active_jobs: $design_categoryのアクティブなjobは2つ';

        is $design_category->get_active_jobs({ rows => 1 })->count, 1, 'get_active_jobs: rowsパラメータで 1 を指定したので 1つだけ習得';
    }
 }

done_testing;

それでは実行してみましょう。

$ prove -l t/jobeet_schema_result_category.t

t/jobeet_schema_result_category.t .. ok
All tests successful.
Files=1, Tests=8,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.58 cusr  0.07 csys =  0.68 CPU)
Result: PASS

全てのテストが成功して、緑色の文字列があなたを祝福してくれました。 あなたのアプリケーションは確実に堅守な物になりました。素晴らしいです。

全てのテストを実行する

今日頑張ってテストを書いたので、t/ ディレクトリの下には

の5つのテストがある状態になりました。 全てのテストが成功し、プロジェクトが健全な状態にあるか確かめましょう。

prove コマンドに t ディレクトリを引数として実行するとディレクトリ以下のテストを全て実行する事ができます。 今回は使っていませんが、サブディレクトリにあるテストも実行させる場合には、先程説明した -r オプションを使用します。

$ prove -l t

...

All tests successful.
Files=5, Tests=17,  3 wallclock secs ( 0.04 usr  0.02 sys +  2.34 cusr  0.34 csys =  2.74 CPU)
Result: PASS

全てのテストを pass した事を確認したので、今日はここまでにしましょう。

また明日

アプリケーションのテストが簡単でなくても、今日のチュートリアルをスキップしたい方がいらっしゃるのはわかります。取り組んでいただけば幸いです。

Arkを受け入れることはArkが提供するすばらしい機能すべてを学ぶことだけでなく、Arkが提唱する開発の~哲学~と~ベストプラクティス~でもあります。テストはそれらの1つです。遅かれ早かれ、ユニットテストは時間の節約になります。これらはコードへの確固たる信頼と恐れずにリファクタリングできる自由を与えてくれます。ユニットテストは何かが壊れているときに警告してくれる安全な護衛です。

明日はjobとcategoryモジュール用の機能テストを書きます。それまでは、Jobeetモデルクラス用のユニットテストをさらに書くための時間をとってください。