jcunit's blog

JCUnitの開発日誌(ログ)です。"その時点での"JCUnit作者の理解や考え、開発状況を日本語で書きます。

Builderのフィールドは全部publicでいいのでは

Builderがクラス化された生成子なら、それは本来メソッドの中で使い捨てにされるものだろう。 ならば、フィールドをprivateにする必要はあるのだろうか?

メソッド間、クラス間で本当に受け渡ししないならばprivateでもpublicでもいいのだが、受け渡しをするとしたらどういう時だろう?

それはあるオブジェクト構築の責任をメソッド間やクラス間で分担する場合だ。 メソッドAとメソッドBの間で構築の責任を分担しているのなら、メソッドAがBuilderに行った結果がメソッドBに影響を及ぼすことはあってもいいのではないか。 publicにすることのデメリットは、Builderはその性質上フィールドにfinal修飾子を与えられないことだ。だからメソッドBの中でBuilderの内部状態が壊されるかもしれない。

しかし壊されて困るようなフィールドならBuilderの生成子でfinalなフィールドに代入しておくべきだろう。

この記事に目を止めてくれた人のコメントをお待ちしたい。

IntelliJはParameterizedテストランナーを特別扱いする

@Suiteのついたテストクラスはまとめていろんなクラスにあるテストを実行する。このいろんなクラスというのはSuiteの「子」としてJUnitに扱われる。 @Suiteアノテーションがつけられたクラスの中のあるメソッドIntelliJのテスト結果画面から選択すると、選ばれたメソッドが属するクラスの選ばれたメソッドを実行する。このとき、@Suiteがついたクラス(親側のクラス)はもう関係ない。 このアプローチがうまく行くのは「子」が静的なクラスだからだ。

    @RunWith(Suite.class)
    @Suite.SuiteClasses({ExampleSuite.Test1.class, ExampleSuite.Test2.class})
    public class ExampleSuite {
      public static class Test1 {

ためしに、こういのを作ってみるといい。static修飾子をTest1から取り去るとテストが動かなくなってしまうのがわかる。

ところでParameterizedもSuiteの一種なのだ。 だが、通常のSuiteと異なり、

    
    // Parameterized.Parameters
    new Object[]{
       {"test1", "hello", "world"}, // 0
       {"test2", "scott", "tiger"}  // 1
    }

のような二重配列の中の一個一個の配列(=テストケース)例えば{"test1", "hello", "world"}が「子」に対応するだ。これらからParameterized#createRunnersForChildrenによってテストランナーが生成され、Parameterizedオブジェクトに登録される。 そして、これを一個一個別の静的クラスにすることはできない。動的に静的なクラスを作ることはできませんよね?

IntelliJ@Parameterizedテストのメソッドを個別に実行するように要求されると、実行クラスはSuiteとなる親のクラス(つまり@Parameterizedがついたクラス)、メソッドは指定されたものを実行する。 これだけでは一体、何番目のテストケースを実行するのかわからない。

ではどうするか?プログラム引数として[0]とか[1]とかを渡すのだ。 これがJUnitの側で解釈されて何番目のテストケースが実行されるかが決まる。

なるほど。

で、JCUnit。JCUnitのランナーはこのParameterizedランナーの仕組みに"則って"作られている。"則って"というのは真似して、という意味だ。Parameterizedを継承していないのだ

だから、JCUnitのテスト結果画面から、テストメソッドを一個だけ選んで実行しようとしても動かない。IntelliJがParameterizedだと認識してくれないから、プログラム引数に[1]とか[10]とかをくれないのだ。 Parameterized#createRunnersForChildrenをオーバーライドすればそれで仕事は終わりなのに、なぜか?

createRunnersForChildrenprivateだからだ。ふざけんな。

じゃ、どうするか?ずいぶん前にぐちゃぐちゃ工夫したのだが、実行したいテストメソッド名がtestMethodだとしたらIntelliJの実行ダイアログでメソッド名の欄をtestMethod[10]とかに変更してみてほしい。10番目のテストが実行されるはずだ。

しかし、ブログは書いてみるもので、アタマが整理されてうまいインチキを思いついた。 試してみてうまく行ったら、後日報告することにしたい。

Theories runner

皆さん、JUnitのTheories runnerは使っていますか? 僕は使っていません。

可能な値の組み合わせをすべて<直積>でテストしようとするのでテストケース数が爆発してしまうのと、 一つのテストメソッドに対してそれらの組み合わせを一つのテストケースとして実行してしまうので、 問題が生じた時に追跡・局所化が難しいと思うからです。

まあそうは言っても、JUnitに最初からバンドルされてるランナーだし、爆発してしまうテストケースを 上手に減らすのはJCUnitの存在意義なので、今日はTheoriesランナーをJCUnitで使う方法を考えてみた。

Theoriesのサポートは次回のリリース(0.5.6)に含まれることになる。

    @RunWith(TheoriesWithJCUnit.class)
    public class TheoriesExample1 {
      @DataPoints("posInt")
      public static int[] positiveIntegers() {
        return new int[] {
        1, 2, 3
        };
      }

      @Theory
      public void test1(
          int a,
          int b,
          int c,
          int d
      ) throws Exception {
        System.out.printf("a=%s, b=%s, c=%d, d=%d%n", a, b, c, d);
      }
    }

この例のソースはJCUnitのレポジトリの0.5.x-developブランチにすでにおいてある。

github.com

さて、それぞれ3つの値を持つ4つのパラメタだ。3 x 3 x 3 x 3=81通りのパターンを実行するのが通常のTheoriesランナー。 しかし、TheoriesWithJCUnitが出力するのは以下の9通りだ。

    a=1, b=1, c=3, d=3
    a=1, b=2, c=2, d=1
    a=1, b=3, c=1, d=2
    a=2, b=1, c=1, d=1
    a=2, b=2, c=3, d=2
    a=2, b=3, c=2, d=3
    a=3, b=1, c=2, d=2
    a=3, b=2, c=1, d=3
    a=3, b=3, c=3, d=1

それでいて、驚くべし、任意の2つの因子(属性)を取り出して見ると可能な組み合わせはすべて網羅されているはずだ。 制約の取り扱いなどを含むもう少し複雑な例もレポジトリにおいてあるので興味のある方は参照してみてほしい。

github.com

一点だけ注意。 この例に含まれる@TupleGenerationアノテーションメソッドに与えることができるがスタンダードなJCUnitで使う同名のアノテーションとは別物(別パッケージにおいてある。クラスとフィールドにしか付けられない)だ。

0.6.0までの間に統合するかもしれないが、少なくとも今は違うものなので気をつけてほしい。

0.5.5リリース

なんとかかんとか、0.5.5をリリースした。 発端は

uehaj.hatenablog.com

JCUnitが持っているプリミティブ用のデフォルト値を外部から取りやすいようにするというだけのことだったのだが、プラグインのインタフェースがあんまりよろしくないから変えないと、とか、この際だからRunnerもユーザが使えるようにするか、とかなってずいぶん手間取ってしまった。

MavenのCoordinateは以下のとおり。

    <dependency>
      <groupId>com.github.dakusui</groupId>
      <artifactId>jcunit</artifactId>
      <version>0.5.5</version>
    </dependency>

リリースノートにも書いたが、@FactorFieldの静的インナークラスDefaultLevelsintLevelsdoubleLevels等のメソッドを作ったのでそこからJCUnitのビルトインされたデフォルト値がとれる。

ということで、フィードバックをお待ちしております。>uehajさん。

その他の変更は以下の通り。 詳しくはリリースノートを見てほしい。

  • Issue-#21: Organize documentation
  • Issue-#22: Expose default values of @FactorField
  • Issue-#23: Separate annotation system from the other parts of JCUnit

JCUnitを支える技術、あるいは再帰もループも用いない順列・組み合わせ列挙のアルゴリズム

今回の話はレポートで組み合わせとか順列の列挙をするプログラムを作る課題をやってる学生さんには面白い話かもしれない。

JCUnitは内部的にcombinatoradix(旧称enumerator)と呼ばれる私が作ったライブラリを使っている。

github.com

JCUnitは性質上、かなりヘビーに組み合わせや順列や直積の列挙を行う。そこで、専用の組み合わせ列挙の仕組みを持っている。 もちろん、誰かが作ったライブラリを利用してもいいのだがそうしなかった理由は2つある。

  • そもそも外部への依存関係は最小にする必要がある。外部ライブラリを利用すると、そのライブラリ自体のテストにJCUnitが使えなくなるのはもとより、そのライブラリを使用した製品のテストの際にライブラリバージョンによる挙動の違いにより、テスト結果が変化する虞がある。
  • 組み合わせを列挙してくれるライブラリは多くあるが、その際に「どのような順番で列挙するか」が明確なものは見つけられなかった。あるかもしれないが、列挙される要素の集合にa.equals(b)になるような組があった場合の挙動がどうなるか?など不安が多い。

JCUnitの場合、どんな順番で列挙がされるかは非常に重要で、これがないとテストの再現性が損なわれる。

combinatoradixは、「階乗進数」という特殊な記数法を使って、順列の列挙を行う。[1] 5個の要素から3個を選んで並べるとき、そのやり方は5P3=60通りだ。 この時、下からひと桁目は0-4(5進数)、二桁目は0-3(4進数)、三桁目は0-2(3進数)にすると0-234までで60通りの数を表現できる。

2nd 1st 0th
0 0 0
1 1 1
2 2 2
(n/a) 3 3
(n/a) (n/a) 4
         0   1   2   3    4
        10  11  12  13   14
        20  21  22  23   24
        30  31  32  33   34
       100 101 102 103  104
       110 111 112 113  114
       120 121 122 123  124
       130 131 132 133  134
       200 201 202 203  204
       210 211 212 213  214
       220 221 222 223  224
       230 231 232 233  234

記号[A,B,C,D,E]から3個を取る順列をこの60個の数字に割り当てる。 割り当て方だが、下から順に、その桁の数字に対応する要素を取りさってゆけば良い。例えば。211なら

    0th = 1:   [A,B,C,D,E] -> "B" (残り:[A,C,D,E])
    1st = 1:   [A,C,D,E] -> "C" (残り:[A,D,E])
    2nd = 2:   [A,D,E] -> "E" (残り:[A,D])

つまり[B,C,E]が対応する値となる。

似た様な(しかしもう少し複雑な)記数法を考えると、組み合わせや重複を許す組み合わせについて同じことができる。

この方法は数の計算と要素の対応付けを行うので、可能な組み合わせをすべて列挙するだけなら、再起やループを使ったほうがずっと早いはずだ。 しかし、際立った特徴がある。

例えば「AからZの文字のうち異なる10文字からなる単語を作り、それらを辞書順にならべたときに30億22番目にある単語はなにか?」という問に答えるのは1マイクロ秒くらいでできる。最初からじゅんじゅんにたどっていく必要がないので。 この性質は大量の組み合わせを分散処理や並列処理で高速化して処理したい場合に有効なはずだ。つまり10億通りの組み合わせがあることがわかっていて(100P3とか1000C4とか簡単に公式で求まる)それぞれについて何らかの処理を行いたいのなら、各ノードはその組み合わせの総数(100P3とか1000C4とか)までの数のうち、ノード数で割った余りが自分のノード番号に等しいものだけを処理すればよいのだ。

  • [1] 偉そうに書いてしまったが、この方法を考えついたあとに誰かがすでにやってそうなもんだと思い調べてみてこういう名前が付いているの知った。不勉強を恥じる次第

Pluginインタフェースの整理

こちらのエントリでご紹介いただいたときに「デフォルトで設定されている各型ごとの水準一覧を取る手段がない」という指摘も頂いた。

uehaj.hatenablog.com

それ自体は、なんとかなりそうで、難しいものでもないのでこんなチケットを作った。

github.com

で、こんなふうに

  class DefaultValues {
    public static final DefaultValues INSTANCE = new DefaultValues();

    @FactorField
    public final Object defaultValues = null;

    private DefaultValues() {
    }

    public boolean[] booleanLevels() {
      return get().booleanLevels();
    }

    public byte[] byteLevels() {
      return get().byteLevels();
    }

    public char[] charLevels() {
      return get().charLevels();
    }
    // 大体こんな雰囲気になるとは思うが、最終的には変更になりそうなので、注意。
  }

jcunit/FactorField.java at 0.5.x-develop · dakusui/jcunit · GitHub

すればよかろうと思う。 が、LevelsProviderプラグインのインタフェースもついでに見直そうと思うとこれがはまるはまる。 しかし、0.6.0は安定版にしたいので、今、綺麗にするしかない。

0.5.5のRelease contentsはこのIssue-#22、各プラグインインタフェースの整理、あとはドキュメントの整理ってあたりになるかなあ。今週末に出したいところだ。