postgresqlを使用したdbUnitテストを行った時に、DBUnitのデフォルトのDataFactoryであるPostgresqlDataTypeFactoryを使用すると思うのですが、postgresqlのテーブルの中に特殊データ型に該当するカラムがあると上手く持ってこれない事象が発生しました。
特殊データ型というのは一般的なDBにあるような型(int,timestamp,varcharとか)ではなく、postgresql特有で定義されている型の事です。(_text、tsvectorとか)
https://www.postgresql.jp/document/9.4/html/datatype.html
事象
Spring batchのRepositoryTestを特殊データ型のカラムが含まれているテーブルに対してdbUnitを用いてSetupしようとしたときにエラーが発生。
対象のテーブル
postgresql公式のチュートリアルにあるテーブル群を使っています。
PostgreSQL Sample Database
今回はfilmテーブルからのSELECTをテストします。
filmテーブル

この中でpostgresqlの特殊データ型に属するのは
- mpaa_rating
- _text
- tsvector
になります。それ以外は通常のやり方でJavaとのマッピングが可能です。
http://beanql.osdn.jp/type_map.html
SQL
Mybatisの定義です。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="itouoti.spring.batch.repository.jobsRepository.job1.Job1Repository"> <select id="sarchfilmData" resultType="itouoti.spring.batch.repository.commonDTO.FilmDTO" parameterType="map"> <![CDATA[ SELECT film_id AS filmId, title, description, release_year AS releaseYear, language_id AS languageId, rental_duration AS rentalDuration, rental_rate AS rentalRate, length, replacement_cost AS replacementCost, rating, last_update AS lastUpdate, special_features AS specialFeatures, fulltext FROM film WHERE film_id = #{filmId} ]]> </select> </mapper>
テストコード
DBUnitを使ってエクセルファイルからテーブルデータをセットアップします。
package itouoti.spring.batch.repository.jobsRepository.job1; import *; /** * Theoriesテスト */ @RunWith(Theories.class) @ContextConfiguration(locations = { "classpath:META-INF/jobs/job1/job1Test.xml" }) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @Transactional @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class) @Slf4j /** * job1Repository sarchfilmData Test * @author itouoti */ public class Job1RepositorySerchFilmDataTest { @ClassRule public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule(); @Rule public final SpringMethodRule springMethodRule = new SpringMethodRule(); @Inject Job1Repository target; @Inject ResourceLoader resourceLoader; @Inject JdbcTemplate jdbcTemplate; @Inject TransactionAwareDataSourceProxy masterDataSource; @Value("${xlsx.expectedDataFile}") String xlsxExpectedDataFile; // エクセルファイルパス private static String xlsxPath = "classpath:itouoti/spring/batch/repository/jobsRepository/job1/"; // テストパターン用エクセルオブジェクト private static IDataSet xlsDataSet; // assertエラー格納用List private static List<AssertionError> assertionErrors = new ArrayList<AssertionError>(); private static String jdbcProps = "classpath:batch-application.properties"; private static Properties props; /** * テストメソッド起動前設定 * @throws Exception 例外 */ @BeforeClass public static void setUpBeforeTest() throws Exception { log.info("□sarchfilmData test is start"); // xlsxファイルの読み込み File file = ResourceUtils.getFile(xlsxPath + "testList.xlsx"); xlsDataSet = new XlsDataSet(file); // propertiesファイルの読み込み props = LoadProperties.load(jdbcProps); // 外部キー制約の解除 String disableAlter = "ALTER TABLE film DISABLE TRIGGER ALL"; ExecSQL.execSQLExecSQL(props, disableAlter, false); } /** * テスト毎起動前設定 * @throws Exception 例外 */ @Before public void setUp() throws Exception { } /** * テスト毎検証後処理 */ @After public void cleanUp() { // テスト後の後処理があれば記述 } /** * テストメソッド実行後処理 * @throws Exception 例外 */ @AfterClass public static void creanUpAfterTest() throws Exception { // テスト後の後処理があれば記述 if (assertionErrors.size() > 0) { for (AssertionError e : assertionErrors) { log.error(e.getMessage()); e.printStackTrace(); } log.error(assertionErrors.size() + "件のAssertionErrorが発生しています"); fail(assertionErrors.size() + "件のAssertionErrorが発生しています"); } log.info("□sarchfilmData test is end"); // 外部キー制約の設定 String disableAlter = "ALTER TABLE film ENABLE TRIGGER ALL"; ExecSQL.execSQLExecSQL(props, disableAlter, false); } /** * job1Repository.sarchfilmDataテスト * @throws Exception DBUnitのxlsx読み込み例外 */ @Theory @DatabaseSetup("classpath:itouoti/spring/batch/repository/jobsRepository/job1/setUp_data.xlsx") public void sarchfilmDataTest( Job1RepositorySerchFilmDataParam testParam) throws Exception { log.info("■ TestNo." + testParam.no + " sarchfilmData test is start"); try { // STEP1 メソッドのテスト Map<String, Object> map = new HashMap<>(); map.put("filmId", testParam.getFilmId()); FilmDTO result = target.sarchfilmData(map); // STEP2 取得件数の検証 assertThat(result.getTitle(), is(testParam.getExpectTitle())); assertThat(result.getDescription(), is(testParam.getExpectDescription())); // STEP3 テーブル状態の検証 ITable actual1 = DBUnitSetUp.getAtualDataTable(masterDataSource, "public.film"); ITable expected1 = DBUnitSetUp.getExpectedDataTable(resourceLoader, xlsxPath + xlsxExpectedDataFile, "film"); org.dbunit.Assertion.assertEquals(new SortedTable(new TrimTable(expected1)), new SortedTable(new TrimTable(actual1), expected1 .getTableMetaData())); } catch (AssertionError e) { // AssertionErrorsに貯める log.error(testParam.no + "Memo = " + testParam.getMemo()); assertionErrors.add(e); } catch (Exception e) { // AssertionErrorsに貯める log.error(e.getMessage()); assertionErrors.add(new AssertionError(e)); } finally { log.info("■ TestNo." + testParam.no + " sarchfilmData test is end"); } } /** * job1Repository.sarchfilmDataテスト用パラメータ生成 * @return sarchfilmData[] * @throws Exception 例外 */ @DataPoints public static Job1RepositorySerchFilmDataParam[] inputTestParams() throws Exception { // テスト用シート名 String testParamSheet = "param"; ArrayList<Job1RepositorySerchFilmDataParam> paramList = new ArrayList<Job1RepositorySerchFilmDataParam>(); try { ITable paramTable = xlsDataSet.getTable(testParamSheet); for (int i = 0; i < paramTable.getRowCount(); i++) { Job1RepositorySerchFilmDataParam param = new Job1RepositorySerchFilmDataParam(); // 試験No param.setNo(TestHelper.toStr(paramTable, i, "no")); // filmId param.setFilmId(TestHelper.toInt(paramTable, i, "filmId")); // 期待値(件数) param.setExpectCount(TestHelper.toInt(paramTable, i, "expectCount")); // 期待値(title) param.setExpectTitle(TestHelper.toStr(paramTable, i, "expectTitle")); // 期待値(description) param.setExpectDescription(TestHelper.toStr(paramTable, i, "expectDescription")); // 備考 param.setMemo(TestHelper.toStr(paramTable, i, "memo")); paramList.add(param); } } catch (Exception e) { e.printStackTrace(); } return paramList.toArray(new Job1RepositorySerchFilmDataParam[paramList.size()]); } }
エラー内容
[2019/06/16 13:47:33] [main] [o.d.util.SQLHelper ] [WARN ] film.special_features data type (2003, '_text') not recognized and will be ignored. See FAQ for more information. [2019/06/16 13:47:33] [main] [o.d.util.SQLHelper ] [WARN ] film.fulltext data type (1111, 'tsvector') not recognized and will be ignored. See FAQ for more information. [2019/06/16 13:47:33] [main] [o.s.t.c.TestContextManager] [WARN ] Caught exception while allowing TestExecutionListener [com.github.springtestdbunit.DbUnitTestExecutionListener@7ba18f1b] to process 'before' execution of test method [public void itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataTest.sarchfilmDataTest(itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataParam) throws java.lang.Exception] for test instance [itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataTest@581d969c] org.dbunit.dataset.NoSuchColumnException: film.SPECIAL_FEATURES - (Non-uppercase input column: special_features) in ColumnNameToIndexes cache map. Note that the map's column names are NOT case sensitive. at org.dbunit.dataset.AbstractTableMetaData.getColumnIndex(AbstractTableMetaData.java:117) at org.dbunit.operation.AbstractOperation.getOperationMetaData(AbstractOperation.java:89) at org.dbunit.operation.AbstractBatchOperation.execute(AbstractBatchOperation.java:150) at org.dbunit.operation.CompositeOperation.execute(CompositeOperation.java:79) at com.github.springtestdbunit.DbUnitRunner.setupOrTeardown(DbUnitRunner.java:183) at com.github.springtestdbunit.DbUnitRunner.beforeTestMethod(DbUnitRunner.java:75) at com.github.springtestdbunit.DbUnitTestExecutionListener.beforeTestMethod(DbUnitTestExecutionListener.java:185) at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:269) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.RunPrepareTestInstanceCallbacks.evaluate(RunPrepareTestInstanceCallbacks.java:64) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.springframework.test.context.junit4.statements.SpringFailOnTimeout.evaluate(SpringFailOnTimeout.java:87) at org.springframework.test.context.junit4.statements.ProfileValueChecker.evaluate(ProfileValueChecker.java:101) at org.junit.experimental.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:232) at org.junit.experimental.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:218) at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:204) at org.junit.experimental.theories.Theories$TheoryAnchor.runWithIncompleteAssignment(Theories.java:212) at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:202) at org.junit.experimental.theories.Theories$TheoryAnchor.evaluate(Theories.java:187) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.springframework.test.context.junit4.statements.ProfileValueChecker.evaluate(ProfileValueChecker.java:101) at org.springframework.test.context.junit4.rules.SpringClassRule$TestContextManagerCacheEvictor.evaluate(SpringClassRule.java:242) at org.junit.rules.RunRules.evaluate(RunRules.java:20) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
special_featuresカラムが原因で失敗しています。
また、warnで認識できない型もいくつか警告されています。
原因
dbUnitの標準のpostgresql用のデータファクトリーである「org.dbunit.ext.postgresql.PostgresqlDataTypeFactory」が対応していない型がテーブルに存在するため。
解決策
PostgresqlDataTypeFactoryをextendsして、カスタムファクトリーを作成する。
test-context.xml
まず、 PostgresqlDataTypeFactory を設定しているところを修正します。
<bean id="dbUnitDatabaseConfig" class="com.github.springtestdbunit.bean.DatabaseConfigBean"> <property name="datatypeFactory"> <bean class="itouoti.spring.batch.testCommon.PsqlArrayDataTypeFactory" /> <!-- <bean class="org.dbunit.ext.postgresql.PostgresqlDataTypeFactory" /> --> </property> </bean>
PsqlArrayDataTypeFactory.java
public class PsqlArrayDataTypeFactory extends PostgresqlDataTypeFactory { public DataType createDataType(int sqlType, String sqlTypeName, String tableName, String columnName) throws DataTypeException { if (sqlType == 2003) { if (sqlTypeName.equals("_text")) { return new ArrayDataType(sqlTypeName, sqlType, false); } } if (sqlType == 1111) { if (sqlTypeName.equals("tsvector")) { return new TsVectorDataType(sqlTypeName, sqlType); } } if (sqlType == 12) { if (sqlTypeName.equals("mpaa_rating")) { return new Mpaa_ratingDataType(sqlTypeName, sqlType); } } return super.createDataType(sqlType, sqlTypeName); } }
警告が出ていたSQLTypeとSQLNameの組み合わせが来た時に専用のクラスに飛ばすようにします。
ArrayDataType.java
参考のStackOverFlowを見てください。
TsVectorDataType.java
package itouoti.spring.batch.testCommon.dataType; import *; public class TsVectorDataType extends AbstractDataType { public TsVectorDataType(String name, int sqlType) { super("tsvector", Types.OTHER, String.class, false); } public Object typeCast(Object value) throws TypeCastException { if (value == null) { return null; } PGobject obj = new PGobject(); obj.setType("tsvector"); try { obj.setValue(value.toString()); } catch (SQLException e) { e.printStackTrace(); } return obj; } public Object getSqlValue(int column, ResultSet resultSet) throws SQLException, TypeCastException { return resultSet.getString(column); } }
文字列を無理やりセットしています。
Mpaa_ratingDataTypeについてもやり方は同じなので詳しくはgithubソースを参照してください。
ちなみにセットアップデータのエクセルはこんな感じです。

結果

成功しました。
ソース
https://github.com/itouoti12/spring-batch
コメントを書く