Asakusa FrameworkのOperator DSLのテストのメモ。
AsakusaFWのオペレーター(ユーザー演算子)は通常のクラスのpublicメソッドなので、JUnitで普通にテストを記述できる。
Operatorクラスに記述するのはユーザー演算子だが、ユーザー演算子のうち、メソッド本体を記述するものがテストの対象になる。
(メソッド本体を記述しないものは、メソッド本体をAsakusaFWが生成するので、テストの対象外で良い。という考え)
Operatorクラスは抽象クラスなので、直接はインスタンス化できない。
Operatorクラス名の末尾に「Impl」の付いた具象クラスがAsakusaFWによって生成される (プログラマーが作ったOperatorクラスを継承している)ので、それを使う。
Update演算子の単体テストの例。
package com.example.operator;
import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; import com.example.modelgen.dmdl.model.Hoge;
/** * {@link ExampleOperator}のテスト. */ public class ExampleOperatorTest { @Test public void edit() { ExampleOperator operator = new ExampleOperatorImpl(); Hoge hoge = new Hoge(); hoge.setValue(123); operator.edit(hoge); assertThat(hoge.getValue(), is(100)); } }
Resultに出力する演算子の場合、テストにはMockResultを使う。
バッチ引数やGroupView等のAPIを使っている場合、その初期化にOperatorTestEnvironmentを使う。
オペレーター内で各種APIを使っている場合、テスト時にはAPIで取得する値をセットする必要がある。
このためにOperatorTestEnvironmentクラスが用意されている。
import org.junit.Rule; import com.asakusafw.testdriver.OperatorTestEnvironment;
public class ExampleOperatorTest { @Rule public OperatorTestEnvironment resource = new OperatorTestEnvironment(); 〜 }
Operatorクラスのメソッドを直接呼び出す方法では、例えばGroupSortの@Keyアノテーションで指定したキーやソート順が正しいかどうかのテストは出来ない(GroupSortの場合、特定のキーのみのデータを自分でソートして渡す必要がある)。
これらをテストしようと思ったら、そのオペレーターを呼び出すだけのFlowPartクラスを作り、そのFlowPartのテストを記述する必要があった。
AsakusaFW 0.10.0で、FlowPartクラスを作らなくてもフローのテストが記述できるようになった。
これをオペレーターのテストに利用することが出来る。
例として、asakusafw-examplesのCategorySummaryOperatorのsummarizeByCategoryメソッドのテストは以下のようになる。
package com.example.operator;
import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.junit.Test; import com.asakusafw.runtime.value.ValueOption; import com.asakusafw.testdriver.FlowPartTester; import com.asakusafw.testdriver.core.ModelVerifier; import com.asakusafw.vocabulary.flow.In; import com.asakusafw.vocabulary.flow.Out; import com.example.modelgen.dmdl.model.CategorySummary; import com.example.modelgen.dmdl.model.JoinedSalesInfo;
/** * {@link CategorySummaryOperator#summarizeByCategory()}のテスト. */ public class CategorySummaryOperatorSummarizeByCategoryTest {
@Test public void test() { FlowPartTester tester = new FlowPartTester(getClass()); List<JoinedSalesInfo> inList = new ArrayList<>(); inList.add(joinedSalesInfo("a", 100, 200)); inList.add(joinedSalesInfo("b", 100, 300)); inList.add(joinedSalesInfo("a", 400, 600)); In<JoinedSalesInfo> in = tester.input("in", JoinedSalesInfo.class).prepare(inList); List<CategorySummary> expectedList = new ArrayList<>(); expectedList.add(categorySummary("a", 100 + 400, 200 + 600)); expectedList.add(categorySummary("b", 100, 300)); Out<CategorySummary> out = tester.output("out", CategorySummary.class).verify(expectedList, new Verifier()); tester.runTest(() -> { CategorySummaryOperatorFactory operator = new CategorySummaryOperatorFactory(); out.add(operator.summarizeByCategory(in).out); }); }
FlowPartTesterのinput, outputメソッドを使って入力データ・検証データを用意する。
通常のFlowPartのテストだとExcelファイルを使って入出力データを用意するが、(Operatorのテストでもそうやって用意してもよいが、)データモデルインスタンスを直接作る事も出来る。
FlowPartTesterのrunTestメソッドにFlow DSLを記述することで、そのフローが実行される。
(Java8のラムダ式を使って、その中にFlow DSLを記述する)
private JoinedSalesInfo joinedSalesInfo(String code, int amount, int price) { JoinedSalesInfo model = new JoinedSalesInfo(); model.setCategoryCodeAsString(code); model.setAmount(amount); model.setSellingPrice(price); return model; } private CategorySummary categorySummary(String code, long amount, long price) { CategorySummary model = new CategorySummary(); model.setCategoryCodeAsString(code); model.setAmountTotal(amount); model.setSellingPriceTotal(price); return model; }
static class Verifier implements ModelVerifier<CategorySummary> { @Override public Object getKey(CategorySummary target) { // 結果データと検証データ(期待値)を結合するキーを返す return target.getCategoryCodeOption(); } @Override public Object verify(CategorySummary expected, CategorySummary actual) { List<String> result = new ArrayList<>(); assertEquals(result, "amount_total", expected.getAmountTotalOption(), actual.getAmountTotalOption()); assertEquals(result, "selling_price_total", expected.getSellingPriceTotalOption(), actual.getSellingPriceTotalOption()); if (!result.isEmpty()) { return result; // 検証エラー時はエラーメッセージを返す } return null; // 検証成功時はnullを返す } } private static <V extends ValueOption<V>> void assertEquals(List<String> result, String name, ValueOption<V> expected, ValueOption<V> actual) { if (!expected.equals(actual)) { result.add(MessageFormat.format("{0}(expected={1}, actual={2})", name, expected, actual)); } } }
ModelVerifierインターフェースを実装し、検証ルールを作成する。
(フローのテストのExcelファイルの「検証ルール」と同等の事をJavaで記述する)
getKeyメソッドで結果データと検証データを結合するキーを返す。
結合するキーが複数項目の場合は、Listにして返すのが楽。(例:「return
Arrays.asList(target.getKey1Option(), target.getKey2Option());
」)
verifyメソッドで検証を行い、検証エラーの場合はエラーメッセージを返す。