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

$shibayu36->blog;

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

Test::Moreのsubtestで困っていること

最近はperlでテスト書く時はTest::Classを使うようにしている。その理由の一つとして、subtestだけのテストだと少しだけ困ることがあるからだ。

具体的には以下の事がある。

  • subtestは書かれている順に実行されるため、前のテストの状態に依存したコードが書かれがち
  • 特定のsubtestだけを実行するのが面倒

前のテストの状態に依存したコードが書かれがち

僕の中では、テストが前のテストの状態に依存しないようにすべきと思っている。各テストの依存度が増えると、その後テストを追加したいときにコードの見る範囲が増え、テストが書きづらくなってしまうからだ。

しかし、subtestは単に書かれた順にテストが実行されるので、前のテストの状態に依存したコードが書きやすいと思っている。例えば以下の様なコードが書かれがち(少し極端な例だが)。

use Test::More;

# insert_entryはDBにinsertする、find_entryはDBから取得するメソッドとします
subtest 'insert' => sub {
    my $entry = insert_entry({
        title => 'title',
        body  => 'body',
    });
    ok $entry;
    is $entry->{title}, 'title';
};

subtest 'find' => sub {
    my $entry = find_entry({
        title => 'title',
    });
    ok $entry;
    is $entry->{body}, 'body';
};

こういうメソッドが書かれてしまった時にfindというsubtestは、insertというsubtestに完全に依存してしまっている。その後、例えばinsert_entryというメソッドが拡張されて、insertというsubtestで作るentry数が増えてしまった時にfindのsubtestが落ちてしまう可能性がある。
するとだんだんinsertというsubtest内のテストを足すのが億劫になってきて、結局テストを増やさないということに繋がる。
テストのカバレッジが減っていくとその後のテストがさらに書きづらくなる。最終的にはテストが書かれないコードが出来上がる。

上のような状態になっていくのは問題なので、テストを書く時は出来るだけ前のテストの状態に依存しづらい状態にしたい。その一つの解決策としてTest::Classを使っている。上のテストだったら、以下のように書ける。

use parent qw(Test::Class);
use Test::More;

# insert_entryはDBにinsertする、find_entryはDBから取得するメソッドとします
sub insert : Tests {
    my $entry = insert_entry({
        title => 'title',
        body  => 'body',
    });
    ok $entry;
    is $entry->{title}, 'title';
}

sub find : Tests {
    insert_entry({
        title => 'title_find',
    });
    my $entry = find_entry({
        title => 'title_find',
    });
    ok $entry;
    is $entry->{body}, 'body';
}

__PACKAGE__->runtests;

この時Test::Classではテストはメソッド名のアルファベット順に実行されるので、find -> insertの順で実行される。これでも前のテストに依存したコードを書こうと思えば書けるのだけど、目に見える順に実行されなくなったので、依存させるための労力が増え、結果前のテストへの依存が減る(と思っている)。

もちろん各テストの独立性をあげるためには、さらにtitleやbodyをrandomに生成するなどのことをするべきである。

特定のsubtestだけを実行するのが面倒

テストがだんだん増えてくると1ファイルのテストを手元で実行する時間が長くなる。これが一定以上長くなってくると、実装を書く -> テストを書く -> テストを実行する -> 実装を書くといった流れを作りにくくなる。

その時に特定のsubtestだけを実行したくなる。上の例で言えばinsertだけ実行するみたいな感じ。

しかしsubtestの場合、これを達成するのが面倒である。Filter subtests by environment variablesのようにすれば、SUBTEST_FILTERというのを指定してinsertのみを実行する事ができるが、このコードを何処かに追加しなければいけない。

Test::Classの場合は特定のメソッドのみ実行するというのがデフォルトでサポートされているのでそれを使えば簡単に達成できる。これが少しだけ便利なので、Test::Classを使っている。

$ TEST_METHOD=insert perl t/entry.t

まとめ

今回はTest::Moreのsubtestだけだと少しだけ困るという話を書いてみた。この解決策としてTest::Classを使うのは微妙かもしれないが、とりあえず最近は上のように書いている。

テストの書き方で心がけていることがいくつかあるので、今後も少しずつ書いていこうと思う。