S-JIS[2011-08-27] 変更履歴

Asakusa Frameworkテストドライバー

Asakusa FrameworkのTestDriver関連のメモ。


概要

AsakusaFWでバッチやジョブフロー等のテストを実行するクラスがテストドライバー。

テストデータの読み込み→テスト実行→実行結果ファイルの読み込み・期待値データと検証ルールの読み込み→検証
の処理を行う。

そして、これらに対してサービスプロバイダー(SPI)機能で独自のクラスを実行させることが出来る。


SourceProvider

SourceProviderは、テストデータを読み込む為のインターフェース。

テスト実行の入力となるデータや、検証に使用する期待値データはSourceProviderを使って読み込まれる。
AsakusaFW 0.2で提供されているのはExcelSheetSourceProvider・JsonSourceProvider。

独自のSourceProviderを作る場合、分かりやすいのは、一つのデータモデルに対して1つのSourceProviderを作ること。


WordCountの入力データ用SourceProviderの例

WordCountの入力データを(Excelファイルからでなく)独自に作ってみる。

このソースファイルはテスト対象と同じEclipseプロジェクト内に格納する。ソースフォルダーは分けておいた方がいいと思う。

src_testdriver/sample/TextSourceProvider.java:

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")」の様に引数を分割して生成する。


WordCountの検証データ用SourceProviderの例

テスト実行が終わった後の検証(比較)用データもSourceProviderで読み込む。

WordCountの場合、実行結果の中身はword_count_modelなので、以下の様になる。

src_testdriver/sample/WordCountSourceProvider.java:

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);
	}
}

SourceProviderのSPIの例

サービスプロバイダー(SPI)で独自クラスを有効にする為に、META-INF/servicesの下に独自クラスを記述したファイルを用意する。

src_testdriver/META-INF/services/com.asakusafw.testdriver.core.SourceProvider:

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

検証ルールを記述する為のインターフェースがVerifyRuleProvider。


WordCountの検証ルールの例

WordCountの検証ルールの例。

src_testdriver/sample/WordCountRuleProvider.java:

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())),
	…
};

VerifyRuleProviderのSPIの例

src_testdriver/META-INF/services/com.asakusafw.testdriver.core.VerifyRuleProvider:

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

検証ルールに関しては、ModelVerifierというインターフェースもある。
VerifyRuleProviderがSPIの機能を使っているのに対し、ModelVerifierはテスト呼び出しのverify()メソッドで指定できる。

		tester.output("out", WordCountModel.class).verify("wordcountmodel:data", new WordCountVerifier());

src_testdriver/sample/WordCountRuleProvider.java:

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


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