DBUnit においては、DB に投入するデータセットは IDataSet と呼ばれる Interface によって規定されている。この記述だけではイメージが分かないが、これを実装したものをいくつか挙げれば、イメージを掴んで頂けると思う。
- CSV ファイルからデータセットを読み込む実装: http://dbunit.sourceforge.net/apidocs/org/dbunit/dataset/csv/CsvDataSet.html
- (Flat) XML ファイルからデータセットを読み込む実装: http://dbunit.sourceforge.net/apidocs/org/dbunit/dataset/xml/FlatXmlDataSet.html
- Excel ファイルからデータセットを読み込む実装: http://dbunit.sourceforge.net/apidocs/org/dbunit/dataset/excel/XlsDataSet.html
要するに IDataSet を実装することにより、どんな媒体からでも DBUnit に対するテストデータを読み込むことができる、ということになる。
問題意識
テストデータを用意する上でダルいのは、そのテストデータの整備、およびそのメンテである。
end-to-end のテストを実施する際に用意すべきテストデータは多数のテーブルに及ぶとともに、ちょっとでも ER に変更が生じれば、個々のテストケースのテストデータを逐一見直さなければならない。ダルい。ダルすぎる。これをアプリケーションで解決したい。アプリケーションで解決するためには、テストデータの表現を CSV や XML ではなく、Java の世界、コードの世界に持ってこなければならない。
このため、テストデータを表現する JavaBean から IDataSet を作成してみた。
実装方針
まず、IDataSet とは何なのかというと、以下 7 つのメソッドの実装を要求する Interface になっている。
- ITable getTable(String tableName)
- 引数のテーブル名が指すテーブルに含まれる一連のデータを表現する ITable を返却する
- ITableMetaData getTableMetaData(String tableName)
- 引数のテーブル名が指すテーブルのメタデータ (カラム数やテーブル名等) を表現する ITableMetaData を返却する
- String[] getTableNames()
- データセットに含まれる、(テストデータを含む)テーブル名の配列を返却する
- ITable[] getTables()
- データセットに含まれる全テーブル情報を返却する
- boolean isCaseSensitiveTableNames()
- テーブル名が caseSensitive だったら true を返却する
- ITableIterator iterator()
- ITableIterator reverseIterator()
- getTable() が返却するテーブル情報を走査するための Iterator を返却する
かなりメンドくさそうだが、メソッド実装用に、DBUnit が多くのデフォルト実装を用意してくれているので、それを利用すれば実装量は大したことはない。
今回の実装方針は、テストデータを JavaBean で表現することとしたため、テストデータを含むテーブルの表現である ITable と、データセットの表現である IDataSet を実装することにした。
JavaBean が個々のテーブルのテストデータ 1 レコードを表現し、その Bean のフィールドがテーブルのカラムを意味するものとする。
実装
実装自体は、YAML で DBUnit の IDataSet を実装していた JYaml - Yaml library for the Java language を参考にした。インスパイアした。された。
最初に ITable 実装を示す。
JavaBean のフィールドをテーブルのカラム名に変換するため、camelCase を snake_case に変換する必要があり、このコンバータとしての Util クラスを挟んでいるが、Google の guavaあたりにもこういう util あったと思うので、適宜使えば良いと思う。
public class JavaBeanTable implements ITable { /** テストデータをレコードとして保持するリスト */ private List<BeanMap> dataList; /** テーブルのメタ情報 */ private ITableMetaData meta; /** * テーブル情報を構築する * @param name テーブル名 * @param dataList テストデータのレコード */ public JavaBeanTable(String name, List<?> dataList) { if ( dataList == null || dataList.isEmpty() ) { throw new IllegalArgumentException(String.format("dataList が空のため、[%s] テーブルのメタデータが取得できません", name)); } this.meta = createMeta(name, dataList.get(0)); List<BeanMap> tmpList = new ArrayList<>(); for (Object e : dataList) { tmpList.add(new BeanMap(e)); } this.dataList = tmpList; } /** * テストデータを追加する * @param dataList テストデータを Java Bean として含むリスト * @return 自分自身 */ public JavaBeanTable appendDataList(List<?> dataList) { List<BeanMap> tmpList = new ArrayList<>(); for (Object e : dataList) { tmpList.add(new BeanMap(e)); } this.dataList.addAll(tmpList); return this; } /** * テストデータのレコード数を返却する * @return テストデータのレコード数 */ @Override public int getRowCount() { return dataList.size(); } /** * 対象テーブルの (DBUnit で言う) メタデータを返却する * @param name テーブル名 * @param record 対象テーブルのレコードを表現する Java Bean。ここで指定されたレコードからメタデータが抽出される * @return */ private <E> ITableMetaData createMeta(String name, E record) { BeanMap beanMap = new BeanMap(record); Column[] columns = new Column[beanMap.keySet().size() - 1]; // -1 をしているのは、".class" フィールドが存在しているため // record からカラム名を抽出 int counter = 0; for (Object obj : beanMap.keySet()) { String key = (String) obj; // class フィールドはテーブルのカラムになり得ないので無視 if ("class".equals(key)) { continue; } // camelCase で表現されたフィールド名を snake_case に変換 String snakeCase = StringCaseConverter.camelToSnake(key); DataType dataType = resolveDataType(beanMap.get(key)); columns[counter++] = new Column(snakeCase, dataType); } return new DefaultTableMetaData(name, columns); } /** * 引数の値に対して、適切な DataType を返却する <br /> * * 基本的には DataType.UNKNOWN を返却すれば良いはずだが、一部適切な型を返却しなければうまくいかない場合があり、 * その際に適宜実装を修正していけばいいんじゃないかな。 * * @param value 属性値 * @return 属性値に対応する DataType */ private DataType resolveDataType(Object value) { if ( value instanceof Boolean ) { return DataType.BOOLEAN; } else { return DataType.UNKNOWN; } } /** * テーブルのメタ情報を返却する */ @Override public ITableMetaData getTableMetaData() { return this.meta; } /** * 指定された行数、列名のデータ値を返却する */ @Override public Object getValue(int row, String columnName) throws DataSetException { if (this.dataList.size() <= row) { throw new RowOutOfBoundsException(); } String camelCase = StringCaseConverter.snakeToCamel(columnName); return this.dataList.get(row).get(camelCase); } }
続いて DataSet そのもの。
public class JavaBeanDataSet implements IDataSet { /** テーブル毎のテーブル情報(テストデータを含む) を保持する Map */ private Map<String, JavaBeanTable> tableMap = new HashMap<String, JavaBeanTable>(); /** * 指定したテーブルに対し、テストデータを追加する * @param tableName テーブル名 * @param dataList テーブルに INSERT するテストデータ (Java Bean) のリスト * @return 自身を返却する (メソッドチェーンで使用することを想定) */ public JavaBeanDataSet addTableData(String tableName, List<?> dataList) { if (!tableMap.containsKey(tableName)) { JavaBeanTable table = new JavaBeanTable(tableName, dataList); tableMap.put(tableName, table); } else { JavaBeanTable table = tableMap.get(tableName); table.appendDataList(dataList); } return this; } @Override public ITable getTable(String tableName) throws DataSetException { return tableMap.get(tableName); } @Override public ITableMetaData getTableMetaData(String arg0) throws DataSetException { return tableMap.get(arg0).getTableMetaData(); } @Override public String[] getTableNames() throws DataSetException { return tableMap.keySet().toArray(new String[tableMap.size()]); } @Override public ITable[] getTables() throws DataSetException { return tableMap.values().toArray(new ITable[tableMap.size()]); } @Override public boolean isCaseSensitiveTableNames() { // Case Sensitivity は不要 return false; } @Override public ITableIterator iterator() throws DataSetException { return new DefaultTableIterator(getTables()); } @Override public ITableIterator reverseIterator() throws DataSetException { return new DefaultTableIterator(getTables(), true); } }
これらを使えば、以下のようにしてデータセットを定義していくことができるようになる。
JavaBeanDataSet dataSet = new JavaBeanDataSet(); dataSet.addTableData("TABLE_A", Arrays.asList(testDataBean1, testDataBean2));
あとは、テストデータ作成用フレームワークというか、いわゆる fixture を作っていけばいいんじゃないかな。