Sunday, February 10, 2013

Theories と Parameterized について思うこと

最近の JUnit は, GitHub 上では Kent Beck さんの手を離れて junit-team さんに移管されたようですね. ご自身も そういう内容のツイート (Feb 6, 2013) をされていました. JUnit の公式 (?) ページ (www.junit.org とか junit.sourceforge.net とか) や Google などの検索結果にそれが反映されるには, しばらく時間がかかりそうではありますが.

それはともかく, 昨年秋にリリースされた JUnit 4.11 につきましては, Javaテストフレームワーク「JUnit 4.11」が公開 という記事 (Nov 26, 2012) に,

また、パラメーター化テスト(Parameterized Tests)では、テストに名称を付ける「@Parameters」アノテーションを利用して個々のテストケースを容易に識別できるようになった。

と記載されているのが目に留まりました.

初歩的なテスト

英小文字を英大文字に変換し, 英大文字を英小文字に変換する, そんなユーティリティメソッドのテストをしてみると仮定してみます. 最も初歩的なテストケースは, たとえば次のようになるでしょうか.

        @Test
        public void test() {
            String actual = FakeStringUtil.reverseCase("foo");
            String expected = "FOO";
            assertThat(actual, is(expected));
        }

あまり複雑なテストになりようのない例なので, これでもいいのかもしれませんが, いろいろなパターンで試してみたい場合ですと, このような入力値を固定したテストケースの書き方は若干どんくさい感じがあります. といっても, わたし自身は, ふだんこういうテストケースの書き方しかしてきませんでしたが.

Theories ランナー

パラメーター化テストをおこなう場合, Theories ランナーを使う方法と Parameterized ランナーを使う方法がありますが, 前者のほうが新しく, また一般的にお奨めのようです. たとえば, 渡辺修司さん (@shuji_w6e) の著書 JUnit実践入門 (技術評論社 2012) p.129 には, 次のように述べられています.

歴史的には、Parameterizedテストランナーのほうが先に実装された機能です。しかし、ほかのxUnit系フレームワークではテストメソッドでパラメータを受け取るようなしくみが一般的であるため、今後はTheoriesテストランナーを使ったほうがよいでしょう。

そこで, まずは Theories を使って, 書いてみることにします.

たとえば次のようになるでしょうか.

    @RunWith(Theories.class)
    public class TheoriesTest {
        
        public static class Fixture {
            private final String input;
            private final String value;
            private Fixture(String input, String value) {
                this.input = input;
                this.value = value;
            }
        }
        
        @DataPoints
        public static Fixture[] getParameters() {
            return new Fixture[] {
                new Fixture("foo", "FOO"),
                new Fixture("BAR", "bar"),
                new Fixture("baz", "BAZ"),
                new Fixture("QUX", "qux")
            };
        }
        
        @Theory
        public void test(Fixture fixture) {
            String actual = FakeStringUtil.reverseCase(fixture.input);
            String expected = fixture.value;
            assertThat(actual, is(expected));
        }
        
    }

テストメソッドにパラメーターを渡すことができ, また渡したいパラメーターをあらかじめ (@DataPoints アノテーションをつけて) まとめて定義しておけるので, 便利そうです.

便利は便利なのですが, ただ, ちょっとだけ気になる点があります. (わたしの場合, 最近は NetBeans に Maven プロジェクトを作成してプログラミングの勉強などをしていて, その環境において 「気になる」 ということなので, ひょっとしたら他の環境であれば 「気になる」 ことではないのかもしれません. が, 一応, 書いておきたいと思います.)

まず, 上記のように 4 種類のパラメーターを用意してテストを実行したとしても, テストケースとしては 1 件となります. たとえば, 4 種類のパラメーターすべてでテストが成功したとしても, テスト結果の一覧には

The test passed.
  TheoriesTest passed
    test passed

としか記載されません. 成功する分にはそれでいいのかもしれませんが, 一部のパラメーターで失敗したような場合, どれなら成功してどれなら失敗するのかが, 一瞬では見えにくかったりします.

たとえば, 上記のテストケースで, 入力値が "BAR" の場合と "QUX" の場合に失敗するような状況にあるとします. このとき, テスト結果には,

No test passed, 1 test caused an error.
  TheoriesTest failed
    test caused an ERROR: test(getParameters[1])

と表示されます. このままですと, インデックスが 1 ということは "BAR" の場合のことを指しているのだなと理解するためには, 失敗レポートの詳細を見るかテストケースのソースコードを読むかしなければなりません. やや一覧性に欠ける気がするのです. また, "BAR" で失敗すると, それ以降のパラメーターはテストしません. そのため, "baz""QUX" の場合はどうなのかが, ただちにはわかりません.

もっとも, レポートの表示のされ方の問題であれば, 自分の好きなようにマッチャーを書くとか Groovy を使うとか, やりようはあるのかもしれません. でも, あるパラメーターで失敗した場合に, それ以降のパラメーターについてはどうなのかがすぐにわからないのは, やや不安を残します. もしかしたら, どういう順番でどういうパラメーターを使うのかを事前に工夫しておけば, そもそもそのような不安が残るはずはない, という可能性もあるかもしれません. テストに使用するデータの選別という課題は, しかし, いまのわたしにはまだちょっと難しくて理解しきれてはいません.

Parameterized ランナー

さて, Parameterized ランナーのほうは, パラメーターをテストメソッドに渡すのではなく, テストクラスのコンストラクターに渡すという方法になります.

たとえば次のようになるでしょうか.

    @RunWith(Parameterized.class)
    public class ParameterizedTest {
        
        @Parameters(name="{0} converts to {1}.")
        public static Iterable<Object[]> getParameters() {
            return Arrays.asList(new Object[][] {
                { "foo", "FOO" },
                { "BAR", "bar" },
                { "baz", "BAZ" },
                { "QUX", "qux" }
            });
        }
        
        private final String input;
        private final String value;
        
        public ParameterizedTest(String input, String value) {
            this.input = input;
            this.value = value;
        }
        
        @Test
        public void test() {
            String actual = FakeStringUtil.reverseCase(input);
            String expected = value;
            assertThat(actual, is(expected));
        }
        
    }

パラメーターの型チェックが弱そうであることと, 二次元配列を使っていることが, 第一印象としては Theories ランナーに負けていますでしょうか. 根拠なく書いてしまいましたが, 何となく他人から嫌がられそうなコードを書いてしまった感触がなきにしもあらずです.

ただ, これの場合, 4 種類のパラメーターすべてをテストしますので, "foo""baz" なら成功して "BAR""QUX" の場合なら失敗するといったことが, わかりやすくなります. また, JUnit 4.11 で追加された機能, 先ほど冒頭に紹介した記事でも述べられていた機能のおかげで, テスト結果の一覧には

2 tests passed, 2 tests failed.
  ParameterizedTest Failed
    test[foo converts to FOO.] passed
    test[BAR converts to bar.] Failed: Expected: is "bar" but: was "BAR"
    test[baz converts to BAZ.] passed
    test[QUX converes to qux.] Failed: Expected: is "qux" but: was "QUX"

と表示されます.

各テストケースにわかりやすい名前をつけたい場合, テストメソッド名で工夫しようとすると, どうしても使うことのできない文字もあったりしますが, このように @Parameterized アノテーションの name 属性を使うと, そのあたりがだいぶ柔軟になります. (とはいえ Groovy を使うほうがもっと簡便で一般的なのかもしれませんが.)

また, パラメーター化テストの便利な点としては, 各テストケースがテスト結果のレポートに表示される順番が, ある程度制御できるし予測できるということを挙げたいと思っています. それを活かすためには, どういった内容のテストなのか, どのパラメーターの場合に成功してどのパラメーターの場合に失敗したのか, などが一目でわかるほうがいいと思っています. そうなると, Parameterized には, Theories にない強みもあるのではないか, そのような気がしてきます.

まとめ

もしかしたら, 今回はじめてパラメーター化テストというものを書いてみた段階ですので, Theories のよさがまだあまりわかっておらず, そのため Parameterized のほうがよさそうに思えているだけかもしれません. また違う感想を抱くようになったら, また書きたいと思います.

以上です. おしまい

1 comment:

  1. こんにちは。

    どちらが良いかはしばらく使わないと分からなかったりしますね。

    ReplyDelete