型引数からその型のオブジェクトを作りたいときにはSupplier
を渡すのがおすすめです
的なことをつぶやいていたので、試しにやってみた。
(どのツイートだったか探してたけど見つからなかったことは秘密です)
【2015/03/11 15:27 追記】
教えてもらいました
@mike_neck つ https://t.co/SGHtJaHte1
— なぎせ ゆうき (@nagise) 2015, 3月 11
@RunWith(Theories.class)
なパラメタライズドテストを実行するときに、
複数件あるデータは@DataPoints
で渡すわけですが、
これを実行するときに入力値と期待値をひとまとめにしたオブジェクトの配列を
渡したかったりします。
くわしくはいろふさんの記事を参照。
例によって、ダラダラ書いているので結論はこちらで、Gistはこちら。
いろふさんの記事ではFixture
というクラスを作っていましたが、
パラメタライズドテストが増えてくると、同じようなクラスを何個も作るのが
面倒になってきて、ついには次のような汎用的なクラスを作りたくなってきます。
public class InputAndExpected<IN, EX> { private final IN in; private final EX ex; public InputAndExpected(IN in, EX ex) { this.in = in; this.ex = ex; } public IN getInput() {return in;} public EX getExpected() {return ex;} }
さて、これを配列使うのがアレな僕が実際にテストで使おうとすると、
やっかいごとが発生します。
@DataPoints public static InputAndExpected<String, Integer>[] testData() { List<InputAndExpected<String, Integer>> list = new ArrayList<>(); list.add(new InputAndExpected<>("hoge", 4)); list.add(new InputAndExpected<>("foo", 3)); return list.toArray(); }
コンパイルは通るのですが、「操作は未チェックまたは安全ではありません」と警告が出てきます。
これをいい感じにコンパイルを通すために、InputAndExpected<String, Integer>
を継承したインナークラスを作ってあげます。
仕方ないので、インナークラスつくります。
@DataPoints @SuppressWarnings("unchecked") public static InputAndExpected<String, Integer>[] testData() { List<TestData> list = new ArrayList<>(); //省略 return list.toArray(new TestData[list.size()]); } private static class TestData extends InputAndExpected<String, Integer> { TestObject(String in, int ex) { super(in, ex); } }
いろふさんが先ほどの記事で最後に書いているように、いちいちlist.add(new TestData("foo", 3));
と書かなくてもいいように、when("foo").then(3)
のようにテストデータを追加できるような快適メソッドを作りたいと思います(車輪の再発明)。
まずはテストデータを一旦保持するコンテナオブジェクトをつくります。
private static Generator<TestData> data = Generator.provider(TestData::new);
次にテストデータを追加していく部分を記述します。
@DataPoints public static InputAndExpected<String, Integer> testData = data.when("hoge").then(4) .when("foo").then(3) .when("bar").then(3) .toArray();
Generator
などというインターフェースはないのでつくります。
public interface Generator<OBJ> { }
when(String)
→then(Integer)
→when(String)
→…→toArray()
という順番で呼び出したいので、
then
だけのメソッドを持つインターフェースをつくります。Generator
に1.のインターフェースを返すwhen
というメソッドを付与します。Generator
にOBJ[]
を返すtoArray
というメソッドを付与します。
public interface Generator<OBJ> { Expecting when(); OBJ[] toArray(); public static Interface Expecting<O> { Generator<O> then(); } }
O
はInputAndExpected<IN, EX>
型であるので、その情報を付与します。
public interface Generator<OBJ extends InputAndExpected<IN, EX>> { Expecting when(); OBJ[] toArray(); public static Interface Expecting<O extends InputAndExpected<I, E>> { Generator<O> then(); } }
when
はIN
を引数に取ります。then
はEX
を引数に取ります。Generator
とExpecting
にIN
とEX
の型情報が足らないので追加します。
public interface Generator<OBJ extends InputAndExpected<IN, EX>, IN, EX> { Expecting<OBJ, IN, EX> when(IN in); OBJ[] toArray(); public static Interface Expecting<O extends InputAndExpected<I, E>, I, E> { Generator<O, I, E> then(E ex); } }
Generator
のインスタンスを返すstatic
なメソッドをGenerator
に追加します。なお、IN
とEX
は別の型ですので、TestData::new
はBiFunction<I, E, O extends InputAndExpected<I, E>
となります。
public interface Generator<OBJ extends InputAndExpected<IN, EX>, IN, EX> { public static <O extends InputAndExpected<I, E>, I, E> Generator<O, I, E> provider( BiFunction<I, E, O> fun) { return new GeneratorImpl<>(fun); } Expecting<OBJ, IN, EX> when(IN in); OBJ[] toArray(); public static Interface Expecting<O extends InputAndExpected<I, E>, I, E> { Generator<O, I, E> then(E ex); } }
適当に実装クラスをつくります。
public class GeneratorImpl<OBJ extends InputAndExpected<IN, EX>, IN, EX> implements Generator<OBJ, IN, EX> { private final BiFunction<IN, EX, OBJ> fun; private final List<Triple<OBJ, IN, EX>> list = new ArrayList<>(); GeneratorImpl(BiFunction<IN, EX, OBJ> fun) { this.fun = fun; } @Override public Expecting<OBJ, IN, EX> when(IN in) { return ex -> { list.add(new Pair<>(function, in, ex)); return GeneratorImpl.this; }; } @Override public OBJ[] toArray() { } private static class Triple<O extends InputAndExpected<I, E>, I, E> { private final I in; private final E ex; private final BiFunction<I, E, O> fun; private Triple(BiFunction<I, E, O> fun, I in, E ex) { this.fun = fun; this.in = in; this.ex = ex; } private O toObject() { return fun.apply(in, ex); } } }
最後のtoArray
ですが、List
のサイズから配列を作って、追加していってもよいのですが、せっかくなのでStream
を使います。
@Override public OBJ[] toArray() { return list.stream.map(Triple::toObject).toArray(); }
これでは型が合わないので、
IntFunction<OBJ[]>
であるOBJ[]::new
をしたい
ところですが、残念ながら型引数からnew
することはできません。そこで、コンストラクターにIntFunction<OBJ[]>
を渡すようにします。
private final IntFunction<OBJ[]> af; GeneratorImpl(BiFunction<IN, EX, OBJ> fun, IntFunction<OBJ[]> af) { this.fun = fun; this.af = af; } @Override public OBJ[] toArray() { return list.stream.map(Triple::toObject).toArray(af); }
これで、無事にOBJ[]::new
っぽいことができました。
あとは、Generator
インターフェースにも反映させます。
public static <O extends InputAndExpected<I, E>, I, E> Generator<O, I, E> provider( BiFunction<I, E, O> fun, IntFunction<O[]> af) { return new GeneratorImpl<>(fun, af); }
テストコードはこんな感じになります。
@RunWith(Theories.class) public static class SampleTest { @DataPoints public static InputAndExpected<String, Integer>[] TEST_DATA = Generator.provider(TestData::new, TestData[]::new) .when("hoge").then(4) .when("foo").then(3) .when("bar").then(3) .toArray(); public void test(InputAndExpected<String, Integer> data) { assertThat(data.getInput().length(), is(data.getExpected())); } private static class TestData extends InputAndExpected<String, Integer> { protected TestData(String s, Integer i) { super(s, i); } } }
結構すっきりしたコードになったと自己満足にふけることができます。
型引数からnewしたい時のおすすめの方法をためしてみた see http://mike-neck.hatenadiary.com/entry/2015/03/11/151938
結論
Spock使ったほうが楽だったかも(´・ω・`)