Perlテストのリファクタリング的なあれ

zentooo
2011-12-10

はじめに

こんにちはこんにちは!最近アレでアレな zentooo です。
ちょっと前まで自分で書いたテスト用データをDBにほげほげするモジュールの話を書こうかと思っていたのですが、disで有名なあの方に会社で「それはいけてないわ」と言われ、確かに自分でもこれはいけてないわー、という気分になってself-reject!したので今日は予定を変えて全体的にふわっとしたことを書きます。

※書いてしまってから昨日の記事と内容かぶってる!と気づきましたがそのままで

testがモリモリ肥大化する

テスト自体はとりあえず書くことは書くんだけど、何も考えずにテストを書いていると、こんな感じになっちゃったー、ってことがよくありますね。僕もよくあります。

subtest with_case1 => sub {
    my $data_for_test = +{
    };
    # 上のデータからオブジェクト作ったりDBにinsertしたり色々してる...
    # ...

    my $result = method_to_be_tested($data_for_test);

    is($result->{foo}, "foo");
    is($result->{bar}, "bar");
    is($result->{baz}, "baz");
};

subtest with_case2 => sub {
    # このへんおんなじような事前準備をしてる(コピペ)
    my $data_for_test = +{
    };
    # ...
    # ...

    my $result = method_to_be_tested($data_for_test);

    # このへんもなんか同じようなチェックをしている(コピペ)
    is($result->{foo}, "foo");
    is($result->{bar}, "bar");
    is($result->{baz}, "baz");
};

subtest with_case3 => sub {
    # 同じくコピペ

    my $result = method_to_be_tested($data_for_test);

    # 同じくコピペ
};

testのための事前準備をするところと、実際にisとかis_deeplyとかで「期待している値が得られたかどうか」をtestしている部分が、やっていることはほぼ同じなんだけど微妙に値とかだけが違う。「なんか冗長だけどテストだから、まあいいか…」という気分になってしまいがちですが、こういうことを続けていると「テストがなんか冗長で読みづらくなる -> テストをメンテする気が低下する -> テストがメンテされなくなる -> 全体のコード品質が下がる」という恐怖のネガティブスパイラルに突入してしまうので、何とかしたいところです。


あるあるその1(同じコード内で共通処理にまとめる)

こういう場合、まず最初に同じコード単位(つまり.tファイル内で)共通処理をまとめてしまうことが考えられますね。あるあるですね。
test用のヘルパー関数を定義します。

# helper functions

sub prepare_data {
    my $params = shift;
    # 事前準備
}

sub check_result {
    my ($actual, $expect) = @_;
    # 実際のテストの内容を共通化したもの
}

# ここから実際のtest

subtest with_case1 => sub {
    my $data_for_test = prepare_data( +{ ... } );
    my $result = method_to_be_tested($data_for_test);
    check_result($result, +{ ... });
};

# 残りのsubtestも同じような感じに書ける

この時点である程度見た目がきれいになって、「各subtestでは一体何をtestをしたいのか」が一目で分かる感じになっていればとりあえずそれでよかったりもします。


あるあるその2(コードをまたいで共通化する)

さっき書いたようなヘルパー関数を、他のテストコードでも使いたい!という場合は普通にあると思います。
小規模なプロジェクトの場合、t/以下にUtils.pmとかを作っちゃって、各.tの中で

    use t::Utils;

    my $data = t::Utils->foo;
    # 中略
    t::Utils->check($result, $expect);

みたいにしてしまうのもアリかもしれません。(好みの問題ですが、t::Utilsからexportしてもいいです)

でも、普通にテストコードおよびヘルパー関数が増えてくるとt::Utilに全部をぶっ込むという方式では立ち行かなくなるので、t/lib 以下にそのプロジェクト専用のテストライブラリ群を作ってしまうのが一般的だと思います。

t/lib/Test/MyProj/Mock.pm
t/lib/Test/MyProj/APIMock.pm
t/lib/Test/MyProj/Logic/*.pm
t/lib/Test/MyProj/Controller/*.pm

こういうファイル群をダーっと作っていって、各.tファイルでは

    use lib './t/lib';
    use Test::MyProj::Mock;
    use Test::MyProj::APIMock;

    my $mock = Test::MyProj::Mock->new;

    Test::MyProj::APIMock->with_mock_api(sub {
        my $port = shift;
        # 外部APIを使うクライアントのtest.
        # with_mock_apiの中身はTest::TCPだったりTest::Fake::HTTPDだったり
    });

という風に使っていくと。こうしておけば、名前空間をよごすことなく自分のプロジェクト専用のテストライブラリを構築できてらくちんですね。

余談ですが、最近僕がコードをよんでいたTengだと

  • t/Utils.pmにはDBのセットアップなどユーティリティ関数的なものを置き、ついでにuse lib './t/lib'しておく
  • t/lib 以下にはnewするようなものを置く

という使い分けになっていて、各.tファイルでは必ず初めに t/Utils.pmをuseするので、いちいち.tファイルごとに use lib './t/lib' しなくていい、という構成になっていたりします。他の人の書いたt/以下を眺めてみると、testの書き方の参考になりますね。

おわりに

今回は、Perlのtest codeをリファクタリング/共通化する際の一般的なやり方を紹介しました。
「testは書くようになったんだけど、どうしてもテストコードが汚くなってしまう…」「Perlでテストファイルをまたぐ共通コードはどういう風に書くのがふつうなの!?」という方へのヒントになれば幸いです。

明日はdannさんです。お楽しみに!