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では対象となるモデルクラスを指定するので、分かりやすい。