PrepAndExpectedTestCase, and its default implementation DefaultPrepAndExpectedTestCase, is a test case formally supporting prep data and expected data concepts and definitions.
PrepAndExpectedTestCase easily allows defining which are the setup datasets (prep) and the verify datasets (expected).
It conveniently packages a turn-key test setup and verification process in one:
Configure this class in one of two ways:
@Inject private PrepAndExpectedTestCase testCase;
Create it by configuring an instance of its interface (start with DefaultPrepAndExpectedTestCase and extend & override if necessary), injecting a IDatabaseTester and a DataFileLoader using the databaseTester and a dataFileLoader properties (see Configuration Example Using Spring below).
@Before public void setDbunitTestDependencies() { setDatabaseTester(databaseTester); setDataFileLoader(dataFileLoader); }
PrepAndExpectedTestCase has two ways to setup, execute, and clean up tests:
(see Test Examples below)
The following configuration shows customizing DatabaseConfig and enables dependency injecting the created PrepAndExpectedTestCase.
@Configuration @Validated public class DbUnitConfiguration { /** * Extend DefaultPrepAndExpectedTestCase to customize DatabaseConfig. */ private class MyPrepAndExpectedTestCase extends DefaultPrepAndExpectedTestCase { public MyPrepAndExpectedTestCase( final DataFileLoader dataFileLoader, final IDatabaseTester databaseTester) { super(dataFileLoader, databaseTester); } @Override protected void setUpDatabaseConfig(final DatabaseConfig config) { // set properties as needed config.setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true); // set the specific IDataTypeFactory if needed config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new XxxDataTypeFactory()); } } /** * Create dbUnit {@link PrepAndExpectedTestCase} for running dbUnit database * tests. * * @param dataFileLoader * The {@link DataFileLoader} used to load the test's specified * data files. * @param databaseTester * The {@link IDatabaseTester} used to run the tests against the * database. * @return Configured dbUnit {@link PrepAndExpectedTestCase} for running * dbUnit database tests. */ @Bean public PrepAndExpectedTestCase prepAndExpectedTestCase( final DataFileLoader dataFileLoader, final IDatabaseTester databaseTester) { return new MyPrepAndExpectedTestCase(dataFileLoader, databaseTester); } /** * Create dbUnit {@link DataFileLoader} for loading the test's dbUnit data * files. * * @param ddr * Your local class containing the replacement definitions. * @return Configured dbUnit {@link DataFileLoader} for loading the test's * dbUnit data files. */ @Bean public DataFileLoader dataFileLoader(final DbunitDataReplacement ddr) { final Map<String, Object> replacementObjects = ddr.getReplacementObjects(); final Map<String, Object> replacementSubstrings = ddr.getReplacementSubstrings(); return new FlatXmlDataFileLoader(replacementObjects, replacementSubstrings); } /** * Create dbUnit {@link IDatabaseTester}. * * @param dataSource * The {@link DataSource} for the dbUnit test to use. * @return Configured dbUnit {@link IDatabaseTester}. */ @Bean public IDatabaseTester databaseTester(final DataSource dataSource) { final DataSource dataSourceProxy = new TransactionAwareDataSourceProxy(dataSource); final IDatabaseTester databaseTester = new DataSourceDatabaseTester(dataSourceProxy); databaseTester.setTearDownOperation(DatabaseOperation.DELETE_ALL); return databaseTester; } }
/** * Class containing replacement objects and replacement substrings for * substitution in dbUnit datasets. */ @Component public class DbUnitDataReplacement { private final Map<String, Object> replacementObjects = new HashMap<>(); private final Map<String, Object> replacementSubstrings = new HashMap<>(); public DbUnitDataReplacement() { populateReplacementObjects(); populateReplacementSubstrings(); } /** * Make replacement objects and populate the map with them. */ private void populateReplacementObjects() { replacementObjects.put("[IGNORE]", null); replacementObjects.put("[NULL]", null); replacementObjects.put("[TIMESTAMP_TODAY]", TestDatabaseDates.TIMESTAMP_TODAY); replacementObjects.put("[TIMESTAMP_TOMORROW]", TestDatabaseDates.TIMESTAMP_TOMORROW); replacementObjects.put("[TIMESTAMP_YESTERDAY]", TestDatabaseDates.TIMESTAMP_YESTERDAY); } /** * Make replacement substrings and populate the map with them. */ private void populateReplacementSubstrings() { } public Map<String, Object> getReplacementObjects() { return replacementObjects; } public Map<String, Object> getReplacementSubstrings() { return replacementSubstrings; } }
/** * Dates for testing with database dates. */ @Component public class TestDatabaseDates { public static final Period ONE_DAY = Period.ofDays(1); public static final Instant NOW = Instant.now(); public static final Timestamp TIMESTAMP_TODAY = asTimestamp(NOW); public static final Timestamp TIMESTAMP_TOMORROW = asTimestamp(NOW.plus(ONE_DAY)); public static final Timestamp TIMESTAMP_YESTERDAY = asTimestamp(NOW.minus(ONE_DAY)); public static Timestamp asTimestamp(final Instant instant) { return Timestamp.from(instant); } }
Note: use good constant names for table names and table definitions, not generic "TABLEn" as used in these examples.
These examples show:
It is helpful to make classes for common test table configurations as the configuration is usually the same for most tests with some tests slightly deviating. The following examples show some of these options.
A simple class of table names, providing consistency and preventing typos.
Make this a production class when additionally specifying table names in classes such as entities and repositories/DAOs.
public abstract class TableNames { public static final String TABLE3 = "table3"; public static final String TABLE5 = "table5"; }
A simple class of column names, providing consistency and preventing typos.
Make this a production class when additionally specifying column names in classes such as entities and repositories/DAOs.
public abstract class ColumnNames { public static final String COLUMN1 = "column1"; }
For specific ValueComparer configurations, it is helpful to isolate them in one or more classes, possibly organized by table.
public abstract class AppValueComparers { public static final Map<String, ValueComparer> COLUMN1_GREATER = new ColumnValueComparerMapBuilder() .add(ColumnNames.COLUMN1, ValueComparers.isActualGreaterThanExpected) .build(); }
Typically, most tests' VerifyTableDefinitions are the same. Some tests' VerifyTableDefinitions needs may deviate on an ignored column or a specific column ValueComparer.
In this example:
public abstract class VerifyTableDefinitions { public static final VerifyTableDefinition TABLE3 = make(TableNames.TABLE3); public static final VerifyTableDefinition TABLE5 = make(TableNames.TABLE5); public static final VerifyTableDefinition TABLE5_COLUMN1_GREATER = make(TableNames.TABLE5, ValueComparers.COLUMN1_GREATER); private static VerifyTableDefinition make(final String tableName) { return new VerifyTableDefinition(tableName, null); } private static VerifyTableDefinition make(final String tableName, final Map<String, ValueComparer> columnValueComparers) { return new VerifyTableDefinition(tableName, null, columnValueComparers); } private static VerifyTableDefinition make(final String tableName, final ValueComparer defaultValueComparer, final Map<String, ValueComparer> columnValueComparers) { return new VerifyTableDefinition(tableName, defaultValueComparer, columnValueComparers); } }
The above VerifyTableDefinitions example class used static VerifyTableDefinition instances. Some ValueComparers require test-specific values so static instances won't work when reused across tests. In these cases, make a parameterized VerifyTableDefinition factory method to take the needed values.
For example, a test may need to specify a different set of "in values" than other tests, such as with the ConditionalSetBiValueComparer. It uses a ValueFactory to determine which of two ValueComparers to use for each table row, so make a factory method with the needed values parameters.
The following example's factory method takes a list of IDs (called "in values") for a column's values requiring a different ValueComparer than the rest of the rows (called "not in values"). Comparing columns happens as configured:
public abstract class VerifyTableDefinitionFactory { public static VerifyTableDefinition tableName_update(Long... ids) { return make(TableName.TABLE_NAME, ValueComparerMapFactory.makeTableName_updated(ids)); } }
public abstract class ValueComparerMapFactory { public static Map<String, ValueComparer> makeTableName_updated(Long[] ids) { Set<Long> values = new HashSet<>(Arrays.asList(ids)); ValueComparer inValuesValueComparer = ValueComparers.isActualGreaterThanExpected; ValueComparer notInValuesValueComparer = ValueComparers.isActualEqualToExpected; ValueFactory<Long> valueFactory = (table, rowNum) -> { Number id = (Number) table.getValue(rowNum, ColumnName.COLUMN1); return id.longValue(); }; ValueComparer conditionalSetBiValueComparer = new ConditionalSetBiValueComparer<Long>(valueFactory, values, inValuesValueComparer, notInValuesValueComparer); return new ColumnValueComparerMapBuilder() .add(ColumnName.COLUMN1, ValueComparers.isActualGreaterThanOrEqualToExpected) .add(ColumnName.COLUMN2, conditionalSetBiValueComparer) .build(); } }
The test then uses the factory method (tableName_update) instead of a constant.
This test uses the default equality column comparison for all columns - the VerifyTableDefinitions used do not specify any ValueComparers so it defaults to equality.
public class DefaultEqualityComparisonExampleTest { // this path is on classpath, e.g. in src/test/resources private static final String DBUNIT_DATA_DIR = "/dbunit/equality/"; private static final String TABLE3_PREP = DBUNIT_DATA_DIR + "table3-prep.xml"; private static final String TABLE4_PREP = DBUNIT_DATA_DIR + "table4-prep.xml"; private static final String TABLE3_EXPECTED = DBUNIT_DATA_DIR + "table3-expected.xml"; private static final String TABLE5_EXPECTED = DBUNIT_DATA_DIR + "table5-expected.xml"; @Inject private PrepAndExpectedTestCase testCase; @Test public void testExample() throws Exception { // COMMON_TABLE1 and COMMON_TABLE2 are defined in common location, such as parent class // with value such as "src/test/resources/dbunit/common/table1.xml" final VerifyTableDefinition[] verifyTables = { VerifyTableDefinitions.TABLE3, VerifyTableDefinitions.TABLE5 }; final String[] prepDataFiles = { COMMON_TABLE1, COMMON_TABLE2, TABLE3_PREP, TABLE4_PREP }; final String[] expectedDataFiles = { TABLE3_EXPECTED, TABLE5_EXPECTED }; testCase.runTest(verifyTables, prepDataFiles, expectedDataFiles, () -> { // execute test steps that exercise production code // e.g. call repository/DAO, call REST service // assert responses or other values // after this method exits, dbUnit will: // * verify configured tables // * cleanup tables as configured return null; // or an object for use/assert outside the Steps }); } }
This test uses the default equality column comparison for all but one column - "table5"'s VerifyTableDefinition specifies a ValueComparer for "column1".
Note the only differences between this test and the prior test are:
public class ValueComparerComparisonExampleTest { // this path is on classpath, e.g. in src/test/resources private static final String DBUNIT_DATA_DIR = "/dbunit/valuecomparer/"; private static final String TABLE3_PREP = DBUNIT_DATA_DIR + "table3-prep.xml"; private static final String TABLE4_PREP = DBUNIT_DATA_DIR + "table4-prep.xml"; private static final String TABLE3_EXPECTED = DBUNIT_DATA_DIR + "table3-expected.xml"; private static final String TABLE5_EXPECTED = DBUNIT_DATA_DIR + "table5-expected.xml"; @Inject private PrepAndExpectedTestCase testCase; @Test public void testExample() throws Exception { // COMMON_TABLE1 and COMMON_TABLE2 are defined in common location, such as parent class // with value such as "src/test/resources/dbunit/common/table1.xml" final VerifyTableDefinition[] verifyTables = { VerifyTableDefinitions.TABLE3, VerifyTableDefinitions.TABLE5_COLUMN1_GREATER }; final String[] prepDataFiles = { COMMON_TABLE1, COMMON_TABLE2, TABLE3_PREP, TABLE4_PREP }; final String[] expectedDataFiles = { TABLE3_EXPECTED, TABLE5_EXPECTED }; testCase.runTest(verifyTables, prepDataFiles, expectedDataFiles, () -> { // execute test steps that exercise production code // e.g. call repository/DAO, call REST service // assert responses or other values // after this method exits, dbUnit will: // * verify configured tables // * cleanup tables as configured return null; // or an object for use/assert outside the Steps }); } }
As with the examples above, usually each test method requires its own prep and expected data so the test methods will each define their own.
Often, we can define dataset files of test data used across multiple tests, typically master lists and a base set of data useful to multiple tests. As above, place them in separate files (usually by table) for easy reuse.
Then, pass the needed ones in the correct data file array (as shown in the examples).
Release 2.5.2 introduced interface PrepAndExpectedTestCaseSteps and the PrepAndExpectedTestCase#runTest(VerifyTableDefinition[], String[], String[], PrepAndExpectedTestCaseSteps) method. This allows for encapsulating test steps into an anonymous inner class or a Java 8+ lambda.
@Inject private PrepAndExpectedTestCase testCase; @Test public void testExample() throws Exception { final VerifyTableDefinition[] verifyTables = {}; // define tables to verify final String[] prepDataFiles = {}; // define prep files final String[] expectedDataFiles = {}; // define expected files final PrepAndExpectedTestCaseSteps testSteps = () -> { // execute test steps that exercise production code // e.g. call repository/DAO, call REST service // assert responses or other values // after this method exits, dbUnit will: // * verify configured tables // * cleanup tables as configured return null; // or an object for use/assert outside the Steps }; testCase.runTest(verifyTables, prepDataFiles, expectedDataFiles, testSteps); }
When using a version prior to Java 8, either use a class (concrete or anonymous inner class) for PrepAndExpectedTestCaseSteps or the following idiom that uses a try/catch/finally template:
@Inject private PrepAndExpectedTestCase testCase; @Test public void testExample() throws Exception { try { final VerifyTableDefinition[] verifyTables = {}; // define tables to verify final String[] prepDataFiles = {}; // define prep files final String[] expectedDataFiles = {}; // define expected files testCase.preTest(verifyTables, prepDataFiles, expectedDataFiles); // execute test steps that exercise production code // e.g. call repository/DAO, call REST service // assert responses or other values } catch (Exception e) { log.error("Test error.", e); throw e; } finally { // verify configured tables and cleanup tables as configured testCase.postTest(); } }