$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

テストと対応関係

 テスト書きすぎ問題 - hitode909の日記階層が増えるとテストが増える - はこべブログ ♨みたいにテストの話が何個か出たので、ちょっと関係ないけど最近のテストで気をつけていることの一つについて書こうと思う。テストを書くときに気をつけていることとして、そのテストが何をテストしているのかという対応関係を明確にしながらテストを書くということを気をつけている。


 例えば以下の様なクラスがあるとする。

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;

 この時has_faviconfavicon_pathはほとんど同じ条件なので、まとめてテストしようと思ってしまうかもしれない。例えばこんな感じ。

subtest 'favicon_pathがある時' => sub {
    my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
    ok $blog->has_favicon;
    is $blog->favicon_path, '/custom/favicon.ico';
};

subtest 'favicon_pathが無い時' => sub {
    my $blog = Blog->new({});
    ok ! $blog->has_favicon;
    is $blog->favicon_path, '/default/favicon.ico';
};


 しかしこのようにテストを書いてしまうと、もう少しクラスが大きくなっていった時にfavicon_pathのテストがどこでなされているかが少しわかりづらい(今回のクラスの大きさでは明確だが)。もしクラスがもう少し大きくなったり、メソッドの条件が複雑になってくると、ある部分に対応するテストがどこなのかというのがだんだん分かりづらくなっていく。

 この場合どんな問題が起こるか。例えば他の人がhas_faviconに対して機能追加しなければならなくなったとする。その時にもしhas_faviconのテストがどこで行われているか分かりにくいと、機能追加に対応するテスト追加がやりづらく、最悪の場合テストを書かないということに繋がると思う。


 そこで最近はある機能に対応するテストがどこにあるのか、というのが明確になるようなテストの書き方を心がけている。どの程度まで対応させて置くのが良いか、というのは難しい問題であるが、現状のところ

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

ということをしている。そのやり方で書いた上のサンプルのテストは以下のとおり。

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';
    };
};

 この場合、確かにテストの書き方は非常に冗長になったし、やり過ぎなんじゃないかというくらいになったかもしれない。しかしその分、あるメソッドに対応するテストはどこにあるのかが分かりやすくなったと思う。そのため、あるメソッドに機能追加した時にテストが追加しやすいのではないかと思っている。


 もしfaviconがcan_use_faviconというフラグが立っていないと使えないという機能追加をした場合だとテストは以下のように変えられると思う。

use Test::More;
use Test::Mock::Guard;

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

    subtest 'can_use_faviconでなければfalse' => sub {
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        ok ! $blog->has_favicon;
    };

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

subtest 'favicon_path' => sub {
    subtest 'faviconを持っていればそのpathを返す' => sub {
        my $g = mock_guard "Blog", { has_favicon => 1 };
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        is $blog->favicon_path, '/custom/favicon.ico';
    };

    subtest 'faviconを持っていなければデフォルトを返す' => sub {
        my $g = mock_guard "Blog", { has_favicon => 0 };
        my $blog = Blog->new({ favicon_path => '/custom/favicon.ico' });
        is $blog->favicon_path, '/default/favicon.ico';
    };
};

 こんなかんじで対応関係をはっきり分かるようにしておけば、どこにテストを追加すればいいのか明確になりテストを追加しやすいのではないかと思っている。また上の例では二つのメソッドのテストを切り分けておいたお陰でmockとかもしやすくなったように思える。


 あまりうまく説明できていないけど、最近はテストの対応関係というのに気をつけている。クラス設計の仕方とか、読みやすいコードの書き方というのは色々と本もあるし共有されていると思うけど、テストの書き方というのはあまり共有されていないように思う。いろんな人がいろんな考え方でテストの書き方を工夫していると思うので、自分はこうやっているというのがもっと共有されたい。

*1:もしくはTest::Classのテストメソッド