読者です 読者をやめる 読者になる 読者になる

スポーツの秋とテスト

体育の日なので、テストの話を書きます。

テスト書きすぎ問題についてから始まり、テストの書き方ノウハウってあんまり共有されないよねって話があったので、自分も id:shibayu36 さんの話に乗っかるとする。

以下、テストと対応関係 - $shibayu36->blog;からコードを引用しました。

package Blog;
use strict;
use warnings;

sub new {
    my ($class, $args) = @_;
    return bless $args, $class;
}

sub has_favicon {
    my ($self) = @_;
    return !! $self->{favicon_path};
}

sub favicon_path {
    my ($self) = @_;
    return $self->has_favicon ? $self->{favicon_path} : '/default/favicon.ico';
}

1;

↑のクラスのテストを書く時には↓のように書くという。

subtest 'has_favicon' => sub {
    subtest 'favicon_pathがあればtrue' => sub {
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        ok $blog->has_favicon;
    };

    subtest 'favicon_pathが無ければfalse' => sub {
        my $blog = Blog->new({});
        ok ! $blog->has_favicon;
    };
};

subtest 'favicon_path' => sub {
    subtest 'favicon_pathがあればそれを返す' => sub {
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        is $blog->favicon_path, '/custom/favicon.ico';
    };

    subtest 'favicon_pathが無ければデフォルトを返す' => sub {
        my $blog = Blog->new({});
        is $blog->favicon_path, '/default/favicon.ico';
    };
};

以下の様な方針らしい。

  • メソッド単位にsubtestを作り、あるメソッドのテストはその中に書く
  • そのメソッドの中の条件に対応してsubtestを切る

自分の場合、これの逆をやっている。 逆というのは、

  • まず、オブジェクトの中の条件に対応してsubtestを切る
  • 次にメソッド単位にsubtestを作り、あるメソッドのテストはその中に書く

ということだ。↑↑のコードを自分風に書き直すとこうなる。

subtest 'favicon_pathがある時' => sub {
    subtest '#favicon_path' => sub {
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        is $blog->favicon_path, '/custom/favicon.ico', 'favicon_pathがあればそれを返す';
    };

    subtest '#has_favicon' => sub {
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        ok $blog->has_favicon, 'favicon_pathがあればtrue';
    };
};

subtest 'favicon_pathが無い時' => sub {
    subtest '#favicon_path' => sub {
        my $blog = Blog->new({});
        is $blog->favicon_path, '/default/favicon.ico', 'favicon_pathが無ければデフォルトを返す';
    };

    subtest '#has_favicon' => sub {
        my $blog = Blog->new({});
        ok ! $blog->has_favicon, 'favicon_pathが無ければfalse';
    };
};

オブジェクトがどういう状態の時にどう振る舞うのかというテストになるのでオブジェクト自体に着目してる感じで、shibayu36さんの方だと手続きの方に着目が置かれているイメージがある。自分の場合は、spec-BDDみたいなやつだと思う。Given-When-Thenを意識してる。

この場合だと、

  • Given - favicon_pathが初期化時に無い状況
  • When - favicon_pathを実行すると
  • Then - デフォルト値が返る

となる。メソッドが引数を取るとなるとGivenとかWhenがネストすることになる。

こう書くことのメリットとしては、テストファイルの中でテスト対象のオブジェクトの生成処理のが、オブジェクトの状態毎に一箇所に集中できるのがいい。自分が作ったモジュールでTest::More::Hooksというのがあるのだけど、コレを使うとTest::Moreのsubtestで、RSpec風のbefore/afterが使える。CPANにおいてある。

↑↑のコードをさらに書き直すとこうなる。

use Test::More;
use Test::More::Hooks;

subtest 'favicon_pathを与えた時' => sub {
    my $blog;
    before { $blog = Blog->new({ favicon_path => '/custom/favicon.ico' }) };

    subtest '#favicon_path' => sub {
        is $blog->favicon_path, '/custom/favicon.ico', 'favicon_pathがあればそれを返す';
    };

    subtest '#has_favicon' => sub {
        ok $blog->has_favicon, 'favicon_pathがあればtrue';
    };
};

subtest 'favicon_pathが無い時' => sub {
    my $blog;
    before { $blog = Blog->new({}) };

    subtest '#favicon_path' => sub {
        is $blog->favicon_path, '/default/favicon.ico', 'favicon_pathが無ければデフォルトを返す';
    };

    subtest '#has_favicon' => sub {
        ok ! $blog->has_favicon, 'favicon_pathが無ければfalse';
    };
};

このbeforeは、同じネストの深さのsubtestが実行される前に呼ばれるので、毎回$blogがbeforeの中にかいた方法で初期化される。RSpecみたいにインスタンス変数を共有、みたいなことが書けなくてmy $blog;みたいに変数を宣言しなきゃならなくてダサいのが難点だけど、この方がテストするオブジェクトの前準備が増えた時に、どこからどこまでテストなのか?ってのが分かりやすいと思う。

あと、ある程度TDDで開発していく時に最初からオブジェクトのすべての状態に対応したメソッドを記述するのって難しいので、この時はこう動けばいいやと設計した時にGivenに当たるsubtest部分を書いていき、あとからこの場合の処理も追加しよう!と思ったらまたGivenに当たるsubtestを追加していってる。

逆にこの書き方の難点としては、一箇所読んだだけだとあるメソッドの仕様がパッと分からないことだと思う。とにかくあるメソッドの振る舞いだけを俺は知りたいんだ!って人には不便だと思う。あと、このTest::More::Hooksというモジュール、会社で使ってるんだけど俺しか使ってる人が居ないしこの書き方が普及してるわけじゃないから初めて読む人は意味ワカらないと思うのが問題。

RSpecみたいにテスト結果の出力のフォーマットを変えると、仕様書みたいになる!ってのはまだやってない。あと、PerlのTest::Classは何だかんだで結局使ってない。

今日は体育の日なので、お昼まで寝てて、その後午後からずっと昼寝してて、いい一日だった。