S-JIS[2017-12-10] 変更履歴

Asakusa Frameworkオペレーターのテスト

Asakusa FrameworkOperator DSLのテストのメモ。


概要

AsakusaFWのオペレーター(ユーザー演算子)は通常のクラスのpublicメソッドなので、JUnitで普通にテストを記述できる。

Operatorクラスに記述するのはユーザー演算子だが、ユーザー演算子のうち、メソッド本体を記述するものがテストの対象になる。
(メソッド本体を記述しないものは、メソッド本体をAsakusaFWが生成するので、テストの対象外で良い。という考え)


Operatorクラスは抽象クラスなので、直接はインスタンス化できない。

Operatorクラス名の末尾に「Impl」の付いた具象クラスがAsakusaFWによって生成される (プログラマーが作ったOperatorクラスを継承している)ので、それを使う。


Update演算子の単体テストの例。

ExampleOperatorTest.java:

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を使う。


OperatorTestEnvironment

オペレーター内で各種APIを使っている場合、テスト時にはAPIで取得する値をセットする必要がある。
このためにOperatorTestEnvironmentクラスが用意されている。

import org.junit.Rule;

import com.asakusafw.testdriver.OperatorTestEnvironment;
public class ExampleOperatorTest {

	@Rule
	public OperatorTestEnvironment resource = new OperatorTestEnvironment();
〜
}

FlowPartTesterを使う例

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メソッドで検証を行い、検証エラーの場合はエラーメッセージを返す。


Operator DSLへ戻る / AsakusaFW目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま