日々常々

ふつうのプログラマがあたりまえにしたいこと。

Theoriesさんの可読性をなんとか

ということなので、いろふさん早くSpockについてブログ書いて下さい。

mike、mikeなるままに…: Spockで例外のテスト

とか言われたのでTheoriesネタで書きます。

Theoriesさん?

JUnit実践入門 8章 パラメータ化テスト をご参照下さい。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)


……ざっくり言うと、パラメータ化テストは「一つのテストメソッドにパラメータを与えて複数のテストをするもの」です。テストメソッドの引数に入ります。色々方法はあるんですけど、シンプルな形だとこうなります。

import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(Theories.class)
public class TheoriesSample {

    @DataPoints
    public static String[] params = {"hoge", "fuga", "piyo"};

    @Theory
    public void theory(String param) {
        assertThat(param.length(), is(4));
    }
}

これで3ケースのテストがされます。JUnitのレポートには1ケースとして出るので、すこしわかり辛いですが。でもこれだと期待値が固定になってしまうため、パラメータ化の嬉しさがあまりありません。なので期待値もパラメータで渡しましょう。


こうでしょうか?

    @DataPoints
    public static String[] params = {"hoge", "fuga", "piyo", "foo"};

    @DataPoints
    public static int[] lengths = {4, 4, 4, 3};

    @Theory
    public void theory(String param, int length) {
        assertThat(param.length(), is(length));
    }

残念ながら、これだとうまくいきません。Theoriesはパラメータに入ってくるものを全部組み合わせてくれます。つまり4*4の16パターンやってくれるんですね。そして param="foo", length=3 のテストがコケちゃいます。
……この組み合わせ、正直私は使いこなせてないです。なんか直感的じゃないし、何が嬉しいかわかんない(´・ω・`)

Theoriesさんの書き方について

パラメータを複数扱うと途端にTheoriesさんは私の制御下を離れるので、パラメータオブジェクトを作って、それであれをそれします。mike_neckさんはこんな感じで書かれてますね。

    @DataPoints
    public static Regulations[] regulations = Regulations.values();

    @Theory
    public void exceptionThrownValidly (Regulations regulation) {
        // 以下略

個人的にはイケてない気がする…

mike、mikeなるままに…: Spockで例外のテスト

同感です。でも配列が問題と言うわけでもないと思います。*1

ちょっとC#に脱線

ところでC#NUnitさんはTestCase属性(Javaで言うアノテーション)を使ってパラメータ化できます。結構見やすくて好きだったりします。

[TestCase(1, "1")]
[TestCase(3, "Fizz")]
[TestCase(5, "Buzz")]
public void TestFizzBuzz(int input, string expected)
{
    // 以下略

ぐるぐるさんが3年弱前にさくっと通りすぎてた道でした。HAHAHA。

TestCaseSource属性なんてあったのか……これが若干 @DataPoints に近く感じました。

DataPointsの可読性にこだわる

本題。@DataPoints はここまでの例ではフィールドにつけていましたが、別にフィールドでなくメソッドにも付けられます。 public static T[] となっていれば何でもいい感じです。どうでもいいですが、フィールドだとIDEが「未使用フィールドだ!」とか言ってキレたりするので、メソッドにしとくと心の平穏を保てるかもしれません。

パラメータオブジェクトを作るくらいなんだから、パラメータを書きやすくしたいものですよね。最初はこんな感じでしょうか。

    @DataPoints
    public static Fixture[] params = {
            new Fixture(1, "1"),
            new Fixture(3, "Fizz"),
            new Fixture(5, "Buzz"),
    };

new Fixtureとか書くのが面倒なので _ メソッドとか作ります。

    @DataPoints
    public static Fixture[] params = {
            _(1, "1"),
            _(3, "Fizz"),
            _(5, "Buzz"),
    };

    static Fixture _(int param, String expected) {
        return new Fixture(param, expected);
    }

……まずい、例が悪かった。もうこれでいいやとか思ってしまった。


いいや、このままいこう。配列嫌いの方はList使っても良いと思います。

    @DataPoints
    public static Fixture[] params() {
        List<Fixture> list = new ArrayList<>();
        list.add(_(1, "1"));
        list.add(_(3, "Fizz"));
        list.add(_(5, "Buzz"));
        return list.toArray(new Fixture[list.size()]);
    }

Listでいいならこんな風にも書けるけど、うん、どうなんだろう。

    @DataPoints
    public static Fixture[] params = new ArrayList<Fixture>() {{
        add(_(1, "1"));
        add(_(3, "Fizz"));
        add(_(5, "Buzz"));
    }}.toArray(new Fixture[0]);

……って感じで記述量を減らしたりとかやってると、何でも出来るってことに気づきます。

どうせやるなら

パラメタライズドテストではデータが主体であって、そこだけを見て、そこだけを編集すればデータが追加できて、こけたときもそこを見て済むのが良いと思ってます。なので、こんな解答もありじゃないかなーと思ってます。

    @DataPoints
    public static Fixture[] parans = new Builder() {{
        when(1).then("1");
        when(3).then("Fizz");
        when(5).then("Buzz");
        when(0).thrown(IllegalArgumentException.class);
    }}.build();

パラメータの数が増えたりしてくると、こっちはかなり威力を発揮してきます。実際業務でも使ってて、私一人だけど、それなりに快適です。

(求むツッコミ!)らしいので……

  • 例外を ExpectedExceptionルール を使う。
  • DataPoints を上記のの雰囲気で書いてみる。

ざっくり書いたら https://gist.github.com/4397706#file-executionpatternstestbyjava-java になりました。

まとめ

すなおにSpock使え。
いやまじで。アレコレやっても https://gist.github.com/4397706#file-executionpatternstestbygroovy-groovy には敵いそうもありません。

*1:やるにしても「配列とIterable」くらいにしかならないでしょうし。