Overview

PrepAndExpectedTestCase, and its default implementation DefaultPrepAndExpectedTestCase, is a test case formally supporting prep data and expected data concepts and definitions.

Prep data
is the setup data needed in the database for the test to run.
Expected data
is the data to compare with one or more database tables verifying the test ran successfully.

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:

  1. Loads the prep dataset files and inserts their data into the database tables
  2. Runs the test steps (if specified as a runTest() PrepAndExpectedTestCaseSteps parameter)
  3. Verifies table state matches the expected datasets for the specified VerifyTableDefinitions
  4. Cleans up the tables listed in the prep and expected datasets (per the configured teardown operation)

Usage

Configure

Configure this class in one of two ways:

  1. Dependency inject it as its interface into a test class.
    @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).

  2. Extend it in a test class. Obtain IDatabaseTester and DataFileLoader instances (possibly dependency injecting them into the test class) and set them accordingly, probably in a setup type of method, such as:
    @Before
    public void setDbunitTestDependencies()
    {
        setDatabaseTester(databaseTester);
        setDataFileLoader(dataFileLoader);
    }
    

Run Tests

PrepAndExpectedTestCase has two ways to setup, execute, and clean up tests:

  1. Encapsulate the test steps in PrepAndExpectedTestCaseSteps and call the runTest() method. Note, this requires Release 2.5.2 and newer.
  2. Call the configureTest(), preTest(), and postTest() methods. Note there is a preTest() convenience method that takes the same parameters as the configureTest() method; use it instead of using both configureTest() and preTest(). Where the test calls those methods depends on data needs:
    • For the whole test case, i.e. in setUp() and tearDown() or @Before and @After.
    • In each test method.
    • Or some combination of both test case setup/teardown and test methods.

(see Test Examples below)

Configuration Example Using Spring

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

Test Examples

Note: use good constant names for table names and table definitions, not generic "TABLEn" as used in these examples.

These examples show:

  1. Before the test runs, insert into the database the dataset contents of the files (represented by the constants COMMON_TABLE1, COMMON_TABLE2, TABLE3_PREP, TABLE4_PREP) as configured by the setup operation (defined outside of the test).
  2. After the test runs, verify table3 and table5 contents are the same as the expected data files and according to the VerifyTableDefinitions.
  3. After the test runs, cleanup tables as configured by the teardown operation (defined outside of the test).

Common Classes for Examples

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.

TableNames

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

ColumnNames

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

ValueComparers

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

VerifyTableDefinitions

Static VerifyTableDefinition Instances

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:

  • "table3" in this class has the same configuration for any test, represented by the TABLE3 constant.
  • "table5" in this class has two configurations:
    1. TABLE5 has all columns using equality comparison
    2. TABLE5_COLUMN1_GREATER has all columns using equality comparison except COLUMN1 using ValueComparers.isActualGreaterThanExpected, verifying the COLUMN1 actual value results in a larger value than the expected value.
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);
    }
}
Test-Specific VerifyTableDefinitions

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:

  • COLUMN1 uses the specified "isActualGreaterThanOrEqualToExpected"
  • COLUMN2 uses the specified "ConditionalSetBiValueComparer",
    • Table rows with an ID in the specified list will use the "inValuesValueComparer" for it, which is "isActualGreaterThanExpected", verifying the value changed and increased.
    • Table rows without an ID in the specified list will use the "notInValuesValueComparer" for it, which is "isActualEqualToExpected", verifying the value did not change.
  • The remaining columns will use the default, which is equality comparison, verifying the value did not change.
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.

Using Default Equality Column Comparison

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

Using ValueComparer Column Comparison

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:

  1. This test uses VerifyTableDefinitions.TABLE5_COLUMN1_GREATER instead of VerifyTableDefinitions.TABLE5
  2. The directory location of the prep and expected files
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
        });
    }
}

Sharing Common (but not all) Prep or Expected Data Among Test Methods

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).

Java 8+ and Anonymous Interfaces

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