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