org.jboss.seam.mock.DBUnitSeamTest Maven / Gradle / Ivy
Show all versions of jboss-seam Show documentation
package org.jboss.seam.mock;
import static org.jboss.seam.mock.DBUnitSeamTest.Database.HSQL;
import static org.jboss.seam.mock.DBUnitSeamTest.Database.MYSQL;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.datatype.DataType;
import org.dbunit.dataset.datatype.DataTypeException;
import org.dbunit.dataset.datatype.DefaultDataTypeFactory;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
/**
* Utility for integration testing with Seam and DBUnit datasets.
*
* Subclass this class instead of SeamTest if you need to insert or clean data in
* your database before and after a test. You need to implement prepareDBUnitOperations() and
* add instances of DataSetOperations to the beforeTestOperations and
* afterTestOperations lists. An example:
*
* public class MyTest extends DBUnitSeamTest {
*
* protected void prepareDBUnitOperations() {
* beforeTestOperations.add(
* new DataSetOperation("my/datasets/BaseData.xml")
* );
* beforeTestOperations.add(
* new DataSetOperation("my/datasets/AdditionalData.xml", DatabaseOperation.INSERT)
* );
* }
* ... // Various test methods with @Test annotation
* }
*
*
* Note that DataSetOperation defaults to DatabaseOperation.CLEAN_INSERT if no
* other operation is specified as a constructor argument. The above example cleans all tables defined
* in BaseData.xml, then inserts all rows declared in BaseData.xml, then inserts
* all the rows declared in AdditionalData.xml. This executes before each test method
* is invoked. If you require extra cleanup after a test method executes, add operations to the
* afterTestOperations list.
*
*
* A test class obtains the database connection for loading and cleaning of datasets in one of the following ways:
*
*
*
A TestNG test parameter named datasourceJndiName is provided by the TestNG test runner, which
* automatically calls setDatasourceJndiName() on the test class before a logical test runs.
*
* An instance of a test class is created manually and the setDatasourceJndiName() method is
* called after creation and before a test runs.
*
* A subclass overrides the getConnection() method and returns a custom database connection.
*
*
*
* Binary files can be imported into the database from a binary directory, configured with the TestNG parameter
* binaryDir or by calling setBinaryDir() before a test runs. The binary directory is a classpath
* reference, e.g. my/org/test/package/binarydir. In your DBUnit XML flat dataset, declare the path of your file
* as follows: <MYTABLE MYCOLUMN="[BINARY_DIR]/mytestfile.png"/>
*
*
* Referential integrity checks (foreign keys) will be or have to be disabled on the database connection
* used for DBUnit operations. This makes adding circular references in datasets easier (especially for nullable
* foreign key columns). Referential integrity checks are enabled again after the connection has been used.
*
*
* IMPORTANT: The methods disableReferentialIntegrity(),
* enableReferentialIntegrity(), and editConfig() are implemented for HSQL and MySQL. You need to
* configure the DBMS you are using with the database TestNG parameter or by calling setDatabase()
* before the the test run. If you want to run unit tests on any other DBMS, you need to override the
* disableReferentialIntegrity() and enableReferentialIntegrity() methods and implement them
* for your DBMS. Also note that by default, if no database TestNG parameter has been set or if the
* setDatabase() method has not been called before test runs, HSQL DB will be used as the default.
*
* @author Christian Bauer
*/
public abstract class DBUnitSeamTest extends SeamTest {
public enum Database {
HSQL, MYSQL
}
private LogProvider log = Logging.getLogProvider(DBUnitSeamTest.class);
protected String datasourceJndiName;
protected String binaryDir;
protected Database database = HSQL;
protected boolean replaceNull = true;
protected List beforeTestOperations = new ArrayList();
protected List afterTestOperations = new ArrayList();
private boolean prepared = false;
protected DBUnitSeamTest() {
super();
}
@BeforeClass
@Parameters("datasourceJndiName")
public void setDatasourceJndiName(@Optional String datasourceJndiName) {
if (datasourceJndiName == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Setting datasource name: " + datasourceJndiName);
}
this.datasourceJndiName = datasourceJndiName;
}
public String getDatasourceJndiName() {
return datasourceJndiName;
}
@BeforeClass
@Parameters("binaryDir")
public void setBinaryDir(@Optional String binaryDir) {
if (binaryDir == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Setting binary directory: " + binaryDir);
}
this.binaryDir = binaryDir;
}
public String getBinaryDir() {
return binaryDir;
}
@BeforeClass
@Parameters("database")
public void setDatabase(@Optional String database) {
if (database == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Setting database: " + database);
}
this.database = Database.valueOf(database.toUpperCase());
}
// We don't have a getDatabase() getter because subclasses might use a different Enum!
@BeforeClass
@Parameters("replaceNull")
public void setReplaceNull(@Optional Boolean replaceNull) {
if (replaceNull == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Setting replace null: " + replaceNull);
}
this.replaceNull = replaceNull;
}
public Boolean isReplaceNull() {
return replaceNull;
}
@BeforeMethod
public void prepareDataBeforeTest() {
// This is not pretty but we unfortunately can not have dependencies between @BeforeClass methods.
// This was a basic design mistake and we can't change it now because we need to be backwards
// compatible. We can only "prepare" the datasets once all @BeforeClass have been executed.
if (!prepared) {
log.debug("Before test method runs, preparing datasets");
prepareDBUnitOperations();
for (DataSetOperation beforeTestOperation : beforeTestOperations) {
beforeTestOperation.prepare(this);
}
for (DataSetOperation afterTestOperation : afterTestOperations) {
afterTestOperation.prepare(this);
}
prepared = true;
}
executeOperations(beforeTestOperations);
}
@AfterMethod
public void cleanDataAfterTest() {
executeOperations(afterTestOperations);
}
private void executeOperations(List list) {
if (log.isDebugEnabled()) {
log.debug("Executing DataSetOperations: " + list.size());
}
IDatabaseConnection con = null;
try {
con = getConnection();
disableReferentialIntegrity(con);
for (DataSetOperation op : list) {
prepareExecution(con, op);
op.execute(con);
afterExecution(con, op);
}
enableReferentialIntegrity(con);
} finally {
if (con != null) {
try {
con.close();
} catch (Exception ex) {
log.error("error closing connection", ex);
}
}
}
}
protected static class DataSetOperation {
private LogProvider log = Logging.getLogProvider(DataSetOperation.class);
String dataSetLocation;
ReplacementDataSet dataSet;
DatabaseOperation operation;
protected DataSetOperation() {
// Support subclassing
}
/**
* Defaults to DatabaseOperation.CLEAN_INSERT
*
* @param dataSetLocation location of DBUnit dataset
*/
public DataSetOperation(String dataSetLocation) {
this(dataSetLocation, DatabaseOperation.CLEAN_INSERT);
}
/**
* Defaults to DatabaseOperation.CLEAN_INSERT
*
* @param dataSetLocation location of DBUnit dataset
* @param dtdLocation optional (can be null) location of XML file DTD on classpath
*/
public DataSetOperation(String dataSetLocation, String dtdLocation) {
this(dataSetLocation, dtdLocation, DatabaseOperation.CLEAN_INSERT);
}
/**
* @param dataSetLocation location of DBUnit dataset
* @param operation operation to execute
*/
public DataSetOperation(String dataSetLocation, DatabaseOperation operation) {
this(dataSetLocation, null, operation);
}
public DataSetOperation(String dataSetLocation, String dtdLocation, DatabaseOperation operation) {
if (dataSetLocation == null) {
this.operation = operation;
return;
}
// Load the base dataset file
InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(dataSetLocation);
try {
InputStream dtdInput = null;
if (dtdLocation != null) {
dtdInput = Thread.currentThread().getContextClassLoader().getResourceAsStream(dtdLocation);
}
if (dtdInput == null) {
this.dataSet = new ReplacementDataSet(new FlatXmlDataSet(input));
} else {
this.dataSet = new ReplacementDataSet(new FlatXmlDataSet(input, dtdInput));
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
this.operation = operation;
this.dataSetLocation = dataSetLocation;
}
public IDataSet getDataSet() {
return dataSet;
}
public DatabaseOperation getOperation() {
return operation;
}
public void prepare(DBUnitSeamTest test) {
if (dataSet == null) {
return;
}
log.debug("Preparing DataSetOperation replacement values");
if (test.isReplaceNull()) {
log.debug("Replacing [NULL] placeholder with real null value");
dataSet.addReplacementObject("[NULL]", null);
}
if (test.getBinaryDir() != null) {
if (log.isDebugEnabled()) {
log.debug("Replacing [BINARY_DIR] placeholder with path: " + test.getBinaryDirFullpath());
}
dataSet.addReplacementSubstring("[BINARY_DIR]", test.getBinaryDirFullpath().toString());
}
}
public void execute(IDatabaseConnection connection) {
if (dataSet == null || operation == null) {
return;
}
try {
if (log.isDebugEnabled()) {
log.debug("Executing: " + this);
}
this.operation.execute(connection, dataSet);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public String toString() {
return getClass().getName() + " with dataset location: " + dataSetLocation;
}
}
// Subclasses can/have to override the following methods
/**
* Override this method if you want to provide your own DBUnit IDatabaseConnection instance.
*
* If you do not override this, default behavior is to use the * configured datasource name and
* to obtain a connection with a JNDI lookup.
*
* @return a DBUnit database connection (wrapped)
*/
protected IDatabaseConnection getConnection() {
try {
if (getDatasourceJndiName() == null) {
throw new RuntimeException("Please set datasourceJndiName TestNG property");
}
DataSource datasource = ((DataSource) getInitialContext().lookup(getDatasourceJndiName()));
// Get a JDBC connection from JNDI datasource
Connection con = datasource.getConnection();
IDatabaseConnection dbUnitCon = new DatabaseConnection(con);
editConfig(dbUnitCon.getConfig());
return dbUnitCon;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Execute whatever statement is necessary to either defer or disable foreign
* key constraint checking on the given database connection, which is used by
* DBUnit to import datasets.
*
* @param con A DBUnit connection wrapper, which is used afterwards for dataset operations
*/
protected void disableReferentialIntegrity(IDatabaseConnection con) {
try {
if (database.equals(HSQL)) {
con.getConnection().prepareStatement("set referential_integrity FALSE").execute(); // HSQL DB
} else if (database.equals(MYSQL)) {
con.getConnection().prepareStatement("set foreign_key_checks=0").execute(); // MySQL > 4.1.1
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Execute whatever statement is necessary to enable integrity constraint checks after
* dataset operations.
*
* @param con A DBUnit connection wrapper, before it is used by the application again
*/
protected void enableReferentialIntegrity(IDatabaseConnection con) {
try {
if (database.equals(HSQL)) {
con.getConnection().prepareStatement("set referential_integrity TRUE").execute(); // HSQL DB
} else if (database.equals(MYSQL)) {
con.getConnection().prepareStatement("set foreign_key_checks=1").execute(); // MySQL > 4.1.1
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Override this method if you require DBUnit configuration features or additional properties.
*
* Called after a connection has been obtaind and before the connection is used. Can be a
* NOOP method if no additional settings are necessary for your DBUnit/DBMS setup.
*
* @param config A DBUnit DatabaseConfig object for setting properties and features
*/
protected void editConfig(DatabaseConfig config) {
if (database.equals(HSQL)) {
// DBUnit/HSQL bugfix
// http://www.carbonfive.com/community/archives/2005/07/dbunit_hsql_and.html
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new DefaultDataTypeFactory() {
@Override
public DataType createDataType(int sqlType, String sqlTypeName) throws DataTypeException {
if (sqlType == Types.BOOLEAN) {
return DataType.BOOLEAN;
}
return super.createDataType(sqlType, sqlTypeName);
}
});
}
}
/**
* Callback for each operation before DBUnit executes the operation, useful if extra preparation of
* data/tables is necessary, e.g. additional SQL commands on a per-operation (per table?) granularity
* on the given database connection.
*
* @param con A DBUnit connection wrapper
* @param operation The operation to be executed, call getDataSet() to access the data.
*/
protected void prepareExecution(IDatabaseConnection con, DataSetOperation operation) {
//
}
/**
* Callback for each operation, useful if extra preparation of data/tables is necessary.
*
* @param con A DBUnit connection wrapper
* @param operation The operation that was executed, call getDataSet() to access the data.
*/
protected void afterExecution(IDatabaseConnection con, DataSetOperation operation) {
//
}
/**
* Resolves the binary dir location with the help of the classloader, we need the
* absolute full path of that directory.
*
* @return URL full absolute path of the binary directory
*/
protected URL getBinaryDirFullpath() {
if (getBinaryDir() == null) {
throw new RuntimeException("Please set binaryDir TestNG property to location of binary test files");
}
return getResourceURL(getBinaryDir());
}
protected URL getResourceURL(String resource) {
URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
if (url == null) {
throw new RuntimeException("Could not find resource with classloader: " + resource);
}
return url;
}
/**
* Load a file and return it as a byte[]. Useful for comparison operations in an actual
* unit test, e.g. to compare an imported database record against a known file state.
*
* @param filename the path of the file on the classpath, relative to configured binaryDir base path
* @return the file content as bytes
* @throws Exception when the file could not be found or read
*/
protected byte[] getBinaryFile(String filename) throws Exception {
if (getBinaryDir() == null) {
throw new RuntimeException("Please set binaryDir TestNG property to location of binary test files");
}
File file = new File(getResourceURL(getBinaryDir() + "/" + filename).toURI());
// Get the size of the file
long length = file.length();
if (length > Integer.MAX_VALUE) {
// File is too large
throw new IllegalArgumentException("File is too large: " + file.getName() + " " + length);
}
return Files.readAllBytes(file.toPath());
}
/**
* Implement this in a subclass.
*
* Use it to stack DBUnit DataSetOperation's with
* the beforeTestOperations and afterTestOperations lists.
*/
protected abstract void prepareDBUnitOperations();
}