Asakusa FrameworkのTestDriver関連のメモ。
		
  | 
	|
AsakusaFWでバッチやジョブフロー等のテストを実行するクラスがテストドライバー。
テストデータの読み込み→テスト実行→実行結果ファイルの読み込み・期待値データと検証ルールの読み込み→検証
の処理を行う。
そして、これらに対してサービスプロバイダー(SPI)機能で独自のクラスを実行させることが出来る。
SourceProviderは、テストデータを読み込む為のインターフェース。
テスト実行の入力となるデータや、検証に使用する期待値データはSourceProviderを使って読み込まれる。
AsakusaFW 0.2で提供されているのはExcelSheetSourceProvider・JsonSourceProvider。
独自のSourceProviderを作る場合、分かりやすいのは、一つのデータモデルに対して1つのSourceProviderを作ること。
WordCountの入力データを(Excelファイルからでなく)独自に作ってみる。
このソースファイルはテスト対象と同じEclipseプロジェクト内に格納する。ソースフォルダーは分けておいた方がいいと思う。
		package sample;  | 
		|
		import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.testdriver.core.DataModelDefinition; import com.asakusafw.testdriver.core.DataModelReflection; import com.asakusafw.testdriver.core.DataModelSource; import com.asakusafw.testdriver.core.IteratorDataModelSource; import com.asakusafw.testdriver.core.PropertyName; import com.asakusafw.testdriver.core.SourceProvider;  | 
		|
		public class TextSourceProvider implements SourceProvider { static final Logger LOG = LoggerFactory.getLogger(TextSourceProvider.class);  | 
		|
		private static final String SCHEME = "linemodel"; @Override public <T> DataModelSource open(DataModelDefinition<T> definition, URI source) throws IOException { String scheme = source.getScheme(); if (!SCHEME.equals(scheme)) { LOG.debug("Not a LineModel URI: {}", source); return null; } LOG.info("LineModel: {}", source); List<DataModelReflection> list = new ArrayList<DataModelReflection>(); list.add(create("Hello World")); list.add(create("Hello Asakusa")); list.add(create("TestDriver Hello")); return new IteratorDataModelSource(list.iterator()); }  | 
		まず、「linemodel」というスキーマを定義している。 テスト実行時に指定されたURIがこのスキーマだったら 自分が対象だと認識する。 そして、DataModelSourceを返す。 この中はIteratorになっており、実際のデータを返す。  | 
	
		private static final PropertyName TEXT = PropertyName.newInstance("text"); private DataModelReflection create(String text) { Map<PropertyName, String> map = new HashMap<PropertyName, String>(); map.put(TEXT, text); return new DataModelReflection(map); } }  | 
		DataModelReflectionはデータモデル(DMDL)に相当するクラス。 DataModelReflectionではMapを使って項目と値を保持する。  | 
	
DataModelReflectionにはデータ入力先となるデータモデル(この例ではline_model)と同じ項目名・データ型を使用する。
line_modelのプロパティーは「text」なので、PropertyNameも「text」となる。
仮にプロパティー名が「text_data」の様にアンダーバーで区切られていたら、PropertyNameは「PropertyName.newInstance("text", "data")」の様に引数を分割して生成する。
テスト実行が終わった後の検証(比較)用データもSourceProviderで読み込む。
WordCountの場合、実行結果の中身はword_count_modelなので、以下の様になる。
public class WordCountSourceProvider implements SourceProvider {
	static final Logger LOG = LoggerFactory.getLogger(WordCountSourceProvider.class);
	private static final String SCHEME = "wordcountmodel";
	@Override
	public <T> DataModelSource open(DataModelDefinition<T> definition, URI source) throws IOException {
		String scheme = source.getScheme();
		if (!SCHEME.equals(scheme)) {
			LOG.debug("Not a WordCountModel URI: {}", source);
			return null;
		}
		LOG.info("WordCountModel: {}", source);
		List<DataModelReflection> list = new ArrayList<DataModelReflection>();
		list.add(create("Hello", 3));
		list.add(create("World", 1));
		list.add(create("Asakusa", 1));
		list.add(create("TestDriver", 1));
		return new IteratorDataModelSource(list.iterator());
	}
	private static final PropertyName WORD  = PropertyName.newInstance("word");
	private static final PropertyName COUNT = PropertyName.newInstance("count");
	// WordCountModelに相当するデータを生成
	private DataModelReflection create(String word, int count) {
		Map<PropertyName, Object> map = new HashMap<PropertyName, Object>();
		map.put(WORD,  word);
		map.put(COUNT, count);
		return new DataModelReflection(map);
	}
}
サービスプロバイダー(SPI)で独自クラスを有効にする為に、META-INF/servicesの下に独自クラスを記述したファイルを用意する。
sample.TextSourceProvider sample.WordCountSourceProvider
普通のSPIではクラスファイルとservices配下のファイルをまとめてjarファイル化するのだが、JUnitで実行するときはクラスパスに自動的に含まれるので、特にjarファイル化する必要は無い。
まずはJobFlowのテストで、入力データだけ独自クラスを使用する例。
public class WordCountJobTest {
	@Test
	public void testDescribe() {
		JobFlowTester tester = new JobFlowTester(getClass());
		tester.input("in", LineModel.class).prepare("linemodel:zzz");
		tester.output("out", WordCountModel.class).verify("word_count_model.xls#output", "word_count_model.xls#rule");
		tester.runTest(WordCountJob.class);
	}
}
prepare()の引数で入力データを示すURIを指定する。
「linemodel」が、今回作ったTextSourceProvider内で定義してあるスキーマ。
ただし「linemodel:」の様にスキーマ名だけだと正しいURIではない(エラーになる)ので、適当な文字列をくっつけている。
今回はTextSourceProvider内に固定でデータを入れてしまっているが、本来はURIでデータそのものやデータの位置を示す情報を指定する。
検証ルールを記述する為のインターフェースがVerifyRuleProvider。
package sample;
import java.io.IOException; import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.testdriver.core.DataModelDefinition; import com.asakusafw.testdriver.core.DataModelReflection; import com.asakusafw.testdriver.core.PropertyName; import com.asakusafw.testdriver.core.VerifyContext; import com.asakusafw.testdriver.core.VerifyRule; import com.asakusafw.testdriver.core.VerifyRuleProvider; import com.asakusafw.testdriver.rule.Equals; import com.asakusafw.testdriver.rule.PropertyCondition;
public class WordCountRuleProvider implements VerifyRuleProvider {
	static final Logger LOG = LoggerFactory.getLogger(WordCountRuleProvider.class);
	private static final String SCHEME = "wordcountmodel";
	private static final PropertyName WORD  = PropertyName.newInstance("word");
	private static final PropertyName COUNT = PropertyName.newInstance("count");
	private static final PropertyName[] KEYS = { WORD };
	private static final PropertyCondition<?>[] CONDS = {
		new PropertyCondition<Integer>(COUNT, Integer.class, Arrays.asList(new Equals()))
	};
	@Override
	public <T> VerifyRule get(DataModelDefinition<T> definition, VerifyContext context, URI source) throws IOException {
		String scheme = source.getScheme();
		if (!SCHEME.equals(scheme)) {
			LOG.debug("Not a WordCount URI: {}", source);
			return null;
		}
		LOG.info("WordCountRule: {}", source);
		return new WorDcountVerifyRule();
	}
	private static class WorDcountVerifyRule implements VerifyRule {
		// 読み込まれたデータからキーの部分だけ抽出する
		@Override
		public Map<PropertyName, Object> getKey(DataModelReflection target) {
			Map<PropertyName, Object> results = new LinkedHashMap<PropertyName, Object>();
			for (PropertyName name : KEYS) {
				results.put(name, target.getValue(name));
			}
			return results;
		}
		// データを比較(検証)する
		@Override
		public Object verify(DataModelReflection expected, DataModelReflection actual) {
			if (expected == null) {
				return MessageFormat.format("結果に対する期待値がありません: キー={0}", getKey(actual));
			}
			if (actual == null) {
				return MessageFormat.format("期待値に対する結果がありません: キー={0}", getKey(expected));
			}
			return checkProperties(expected, actual);
		}
		private Object checkProperties(DataModelReflection expected, DataModelReflection actual) {
			List<String> differences = new ArrayList<String>(1);
			for (PropertyCondition<?> condition : CONDS) {
				Object e = expected.getValue(condition.getPropertyName());
				Object a = actual  .getValue(condition.getPropertyName());
				if (!condition.accepts(e, a)) {
					differences.add(MessageFormat.format("\"{0}\"(={1})が正しくありません: {2}",
						condition.getPropertyName(),
						a,
						condition.describeExpected(e, a)
					));
				}
			}
			return differences.isEmpty() ? null : differences;
		}
	}
}
getKey()で、該当データからキー部分のデータだけ抽出する。
そしてverify()が呼ばれる。
CONDSという配列に検証項目・データ型・条件(比較方法)を持たせている。
複数の項目がある場合は配列に要素を追加していく。
private static final PropertyCondition<?>[] CONDS = {
	new PropertyCondition<Integer>(COUNT, Integer.class, Arrays.asList(new Equals())),
	…
};
sample.WordCountRuleProvider
JobFlowのテストで、入力データと期待値データ・検証ルールで独自クラスを使用する例。
public class WordCountJobTest {
	@Test
	public void testDescribe() {
		JobFlowTester tester = new JobFlowTester(getClass());
		tester.input("in", LineModel.class).prepare("linemodel:zzz");
		tester.output("out", WordCountModel.class).verify("wordcountmodel:data", "wordcountmodel:rule");
		tester.runTest(WordCountJob.class);
	}
}
入力のprepare()と同様に、出力のverify()に期待値データのURIと検証ルールのURIを指定する。
検証ルールに関しては、ModelVerifierというインターフェースもある。
VerifyRuleProviderがSPIの機能を使っているのに対し、ModelVerifierはテスト呼び出しのverify()メソッドで指定できる。
		tester.output("out", WordCountModel.class).verify("wordcountmodel:data", new WordCountVerifier());
package sample;
import java.text.MessageFormat; import java.util.Arrays; import sample.modelgen.dmdl.model.WordCountModel; import com.asakusafw.testdriver.core.ModelVerifier;
public class WordCountVerifier implements ModelVerifier<WordCountModel> {
	@Override
	public Object getKey(WordCountModel target) {
		return Arrays.asList(target.getWord());
	}
	@Override
	public Object verify(WordCountModel expected, WordCountModel actual) {
		if (expected == null) {
			return MessageFormat.format("結果に対する期待値がありません: キー={0}", getKey(actual));
		}
		if (actual == null) {
			return MessageFormat.format("期待値に対する結果がありません: キー={0}", getKey(expected));
		}
		if (expected.getCount() != actual.getCount()) {
			return MessageFormat.format("\"{0}\"(={1})が正しくありません: {2}",
				"count", actual.getCount(), expected.getCount()
			);
		}
		return null; // 正常
	}
}
また、VerifyRuleProviderがモデルのプロパティー名や値の取得方法・比較方法を色々抽象化しているのに対し、
ModelVerifierでは対象となるモデルクラスを指定するので、分かりやすい。