Monday, February 11, 2013

Theories で ParametersSuppliedBy を使う

これは 昨日の記事 の続きです. その後, Theories ランナーには, @DataPoints などを使うパターンとは別に @ParametersSuppliedBy などを使うパターンがあることを知りました.

@DataPoints を使うパターン

Theories ランナーを利用する場合の解説としては, @DataPoints を使うパターンが圧倒的に多い気がします.

昨日書いたコードとあまり変わり映えしませんが, だいたい次のようになりましょうか.

[フィクスチャー]

    public class Fixture {
        public final String input;
        public final String expectedValue;
        public Fixture(String input, String expectedValue) {
            this.input = input;
            this.expectedValue = expectedValue;
        }
    }

[テストケース]

    @RunWith(Theories.class)
    public class DataPointsTest {
        @DataPoints
        public static Fixture[] fixture = new Fixture[] {
            new Fixture("foo", "FOO"),
            new Fixture("BAR", "bar"),
            new Fixture("baz", "BAZ"),
            new Fixture("QUX", "qux")
        };
        @Theory
        public void test(Fixture fixture) throws Exception {
            String expected = fixture.expectedValue;
            String actual = FakeStringUtil.reverseCase(fixture.input);
            assertThat(actual, is(expected));
        }
    }

この場合, たとえば入力値が "baz" のときに失敗すると, 最終的にスローされてくる実行時例外のメッセージには, test(fixture[2]) とだけ記載されます. 結果レポートの一覧にもそのように記載されます. インデックスが 2 であるテストデータで失敗したのだとはわかりますが, 具体的にどの値がダメだったのかは, スタックトレースを読むかテストケースのソースコードを読むかしないかぎり, 一目瞭然ではありません.

@ParametersSuppliedBy を使うパターン

これは JUnit4のRunner概説 - penultimate diary という記事 (Jan 14, 2012) を目にして, はじめてその存在に気づきました.

準備作業に若干手間がかかって使いにくいのですが, まずは ParameterSupplier という抽象クラスを継承したクラスを作成しておきます. (フィクスチャー自体は前述のものを再利用します.)

    public class TestDataSupplier extends ParameterSupplier {
        @Override
        public List<PotentialAssignment>
                getValueSources(ParameterSignature signature) {
            return Arrays.asList(new PotentialAssignment[] {
                PotentialAssignment.forValue(
                    "fooがFOOに変換されるかどうか",
                    new Fixture("foo", "FOO")),
                PotentialAssignment.forValue(
                    "BARがbarに変換されるかどうか",
                    new Fixture("BAR", "bar")),
                PotentialAssignment.forValue(
                    "bazがBAZに変換されるかどうか",
                    new Fixture("baz", "BAZ")),
                PotentialAssignment.forValue(
                    "QUXがquxに変換されるかどうか",
                    new Fixture("QUX", "qux"))
            });
        }
    }

getValueSources という抽象メソッドを実装し, テストデータの数だけの PotentialAssignment インスタンスのリストを返すようにします. 上記の TestDataSupplier クラスの中に別途次のようなメソッドを用意しておけば, もう少し簡略化することができると思いますが,

        private PotentialAssignment _(String input, String expected) {
            return PotentialAssignment.forValue(
                String.format("%sが%sに変換されるかどうか",
                        input, expected),
                new Fixture(input, expectedValue)
            );
        }

今回の話題にとって本質的ではないので, とりあえずいいことにします.

で, テストケースのソースコードは, だいたい次のようになりましょうか.

    @RunWith(Theories.class)
    public class ParameterSupplierTest {
        @Theory
        public void test(
                @ParametersSuppliedBy(TestDataSupplier.class)
                Fixture fixture) throws Exception {
            String expected = fixture.expectedValue;
            String actual = FakeStringUtil.reverseCase(fixture.input);
            assertThat(actual, is(expected));
        }
    }

このテストを実行しますと, たとえば入力値が "baz" のときに失敗すると, 最終的にスローされてくる実行時例外のメッセージには, test(bazがBAZに変換されるかどうか) と記載されます. 結果レポートの一覧にもそのように記載されます.

さきほどの @DataPoints を使ったパターンと比べますと, 具体的にどのテストデータで失敗したのかを, だいぶわかりやすく表現することができそうに思えます.

なお, テストメソッドの引数に付加するアノテーションが若干重たい感じがしますので, 次のように自前のアノテーションを用意してそれを使用するのもよいかもしれません.

    @Retention(RetentionPolicy.RUNTIME)
    @ParametersSuppliedBy(TestDataSupplier.class)
    public static @interface TestData {}

そうすれば, このテストクラスは,

    @RunWith(Theories.class)
    public class ParameterSupplierTest {
        @Theory
        public void test(@TestData Fixture fixture) throws Exception {
            // ...
        }
    }

と書くことができますし, (今回の例ではいまいちですが) もっと気の利いたアノテーション名を用いることで, どういった種類のフィクスチャーを使おうとしているのかを伝わりやすくすることもできるんじゃないかと思います. (このあたりは Schauderhaft » More on JUnit Theories という記事 (Feb 7, 2010) を参考にさせていただきました.)

まとめ

抽象クラスを継承するという手間が面倒くさく見えるかもしれません. が, いくつかの種類のフィクスチャーを多少なりとも可視的にかつ再利用しやすく使い分けたいときとか, あるいはテスト結果のレポートにあらわれるメッセージに少しでもわかりやすい文言を表示したいときとか, そういうことを考えますと, @DataPoints ではなく @ParametersSuppliedBy を使うパターンというのもおもしろい気がしました.

とはいえ, Theories ランナーを利用するにせよ, Parameterized ランナーを利用するにせよ, やはり若干冗長なテストコードにならざるをえない感触は拭えません. 渡辺修司さん (@shuji_w6e) の著書 JUnit実践入門 (技術評論社 2012) p.114 によれば,

フィクスチャのセットアップを生成メソッドに抽出すればテストコードの可読性は高くなります。しかしながら、複雑なオブジェクトの生成を行うコードが消えるわけではありません。Javaではフィクスチャのセットアップのようなデータを生成するコードは読みやすくありません。これは、Javaの言語仕様では宣言的なコードを記述しにくいためです。

とのことです.

あるいは, テストの可読性という点では, 最近は Groovy や Spock を使う事例を (Twitter 上では) よく見聞きするようにもなってきました. そちらのほうが今後はますます常識的な選択肢になりつつあるのかもしれません.

1 comment:

  1. こんにちは。

    なかなかここまでやらないので勉強になります!

    ReplyDelete