org.unitils.database.DatabaseModule Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2008, Unitils.org
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.unitils.database;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.unitils.core.Module;
import org.unitils.core.TestListener;
import org.unitils.core.Unitils;
import org.unitils.core.UnitilsException;
import org.unitils.core.config.Configuration;
import org.unitils.core.dbsupport.DefaultSQLHandler;
import org.unitils.core.dbsupport.SQLHandler;
import org.unitils.database.annotations.TestDataSource;
import org.unitils.database.annotations.Transactional;
import org.unitils.database.config.DataSourceFactory;
import org.unitils.database.config.DatabaseConfiguration;
import org.unitils.database.config.DatabaseConfigurations;
import org.unitils.database.config.DatabaseConfigurationsFactory;
import org.unitils.database.transaction.UnitilsTransactionManager;
import org.unitils.database.transaction.impl.UnitilsTransactionManagementConfiguration;
import org.unitils.database.util.Flushable;
import org.unitils.database.util.TransactionMode;
import org.unitils.dbmaintainer.DBMaintainer;
import org.unitils.util.PropertyUtils;
import org.unitils.util.ReflectionUtils;
import static org.unitils.core.util.ConfigUtils.getInstanceOf;
import static org.unitils.database.util.TransactionMode.COMMIT;
import static org.unitils.database.util.TransactionMode.DEFAULT;
import static org.unitils.database.util.TransactionMode.DISABLED;
import static org.unitils.database.util.TransactionMode.ROLLBACK;
import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
import static org.unitils.util.AnnotationUtils.getMethodOrClassLevelAnnotationProperty;
import static org.unitils.util.AnnotationUtils.getMethodsAnnotatedWith;
import static org.unitils.util.ModuleUtils.getAnnotationPropertyDefaults;
import static org.unitils.util.ModuleUtils.getEnumValueReplaceDefault;
/**
* Module that provides support for database testing: Creation of a datasource
* that connects to the test database, support for executing tests in a
* transaction and automatic maintenance of the test database.
*
* A datasource will be created the first time one is requested. Which type of
* datasource will be created depends on the configured
* {@link DataSourceFactory}. By default this will be a pooled datasource that
* gets its connection-url, username and password from the unitils
* configuration.
*
* The created datasource can be injected into a field of the test by annotating
* the field with {@link TestDataSource}. It can then be used to install it in
* your DAO or other class under test.
*
* If the DBMaintainer is enabled (by setting
* {@link #PROPERTY_UPDATEDATABASESCHEMA_ENABLED} to true), the test database
* schema will automatically be updated if needed. This check will be performed
* once during your test-suite run, namely when the data source is created.
*
* If the test class or method is annotated with {@link Transactional} with
* transaction mode {@link TransactionMode#COMMIT} or
* {@link TransactionMode#ROLLBACK}, or if the property
* 'DatabaseModule.Transactional.value.default' was set to 'commit' or
* 'rollback', every test is executed in a transaction.
*
* @author Filip Neven
* @author Tim Ducheyne
* @see TestDataSource
* @see DBMaintainer
* @see Transactional
*/
public class DatabaseModule
implements Module {
/**
* Property indicating if the database schema should be updated before
* performing the tests
*/
public static final String PROPERTY_UPDATEDATABASESCHEMA_ENABLED = "updateDataBaseSchema.enabled";
/**
* Sets all the sequences to the lowest acceptable value.
* This can be defined with the property "sequenceUpdater.sequencevalue.lowestacceptable".
*/
public static final String PROPERTY_RESET_SEQUENCES = "reset.sequences.to.lowest.value.before.test";
/**
* Property indicating whether the datasource injected onto test fields
* annotated with @TestDataSource or retrieved using
* {@link DataSourceWrapper#getTransactionalDataSourceAndActivateTransactionIfNeeded(Object)}
* must be wrapped in a transactional proxy
*/
public static final String PROPERTY_WRAP_DATASOURCE_IN_TRANSACTIONAL_PROXY = "dataSource.wrapInTransactionalProxy";
/* The logger instance for this class */
private static Logger logger = LoggerFactory.getLogger(DatabaseModule.class);
/**
* Map holding the default configuration of the database module annotations
*/
protected Map, Map> defaultAnnotationPropertyValues;
/**
* The datasources with the name as key
*/
protected DataSource dataSource;
/**
* The configuration of Unitils
*/
protected Properties configuration;
/**
* Indicates if the DBMaintainer should be invoked to update the database
*/
protected boolean updateDatabaseSchemaEnabled;
/**
* Indicates whether the datasource injected onto test fields annotated with
*
* @TestDataSource or retrieved using
* {@link DataSourceWrapper#getTransactionalDataSourceAndActivateTransactionIfNeeded} must be
* wrapped in a transactional proxy
*/
protected boolean wrapDataSourceInTransactionalProxy;
/**
* The transaction manager
*/
protected UnitilsTransactionManager transactionManager;
/**
* Set of possible providers of a spring
* PlatformTransactionManager
*/
protected Set transactionManagementConfigurations = new HashSet<>();
// protected String dialect;
private DatabaseConfigurations databaseConfigurations;
// protected DataSourceWrapper wrapper;
protected Map wrappers = new HashMap<>();
/**
* Initializes this module using the given Configuration
*
* @param configuration
* The config, not null
*/
@Override
public void init(Properties configuration) {
this.configuration = configuration;
DatabaseConfigurationsFactory configFactory = new DatabaseConfigurationsFactory(new Configuration(configuration));
databaseConfigurations = configFactory.create();
defaultAnnotationPropertyValues = getAnnotationPropertyDefaults(DatabaseModule.class, configuration, Transactional.class);
updateDatabaseSchemaEnabled = PropertyUtils.getBoolean(PROPERTY_UPDATEDATABASESCHEMA_ENABLED, configuration);
wrapDataSourceInTransactionalProxy = PropertyUtils.getBoolean(PROPERTY_WRAP_DATASOURCE_IN_TRANSACTIONAL_PROXY, configuration);
PlatformTransactionManager.class.getName();
}
/**
* Initializes the spring support object
*/
@Override
public void afterInit() {
// do nothing
}
public void registerTransactionManagementConfiguration() {
for (DataSourceWrapper wrapper : wrappers.values()) {
registerTransactionManagementConfiguration(wrapper);
}
}
public void registerTransactionManagementConfiguration(final DataSourceWrapper wrapper) {
// Make sure that a spring DataSourceTransactionManager is used for transaction management, if
// no other transaction management configuration takes preference
registerTransactionManagementConfiguration(new UnitilsTransactionManagementConfiguration() {
@Override
public boolean isApplicableFor(Object testObject) {
return true;
}
@Override
public PlatformTransactionManager getSpringPlatformTransactionManager(Object testObject) {
return new DataSourceTransactionManager(wrapper.getDataSourceAndActivateTransactionIfNeeded());
}
@Override
public boolean isTransactionalResourceAvailable(Object testObject) {
return wrapper.isDataSourceLoaded();
}
@Override
public Integer getPreference() {
return 1;
}
});
}
public void activateTransactionIfNeeded() {
if (transactionManager != null) {
transactionManager.activateTransactionIfNeeded(getTestObject());
}
}
/**
* Returns the transaction manager or creates one if it does not exist yet.
*
* @return The transaction manager, not null
*/
public UnitilsTransactionManager getTransactionManager() {
if (transactionManager == null) {
transactionManager = getInstanceOf(UnitilsTransactionManager.class, configuration);
}
transactionManager.init(transactionManagementConfigurations);
return transactionManager;
}
/**
* Flushes all pending updates to the database. This method is useful when
* the effect of updates needs to be checked directly on the database.
*
* Will look for modules that implement {@link Flushable} and call
* {@link Flushable#flushDatabaseUpdates(Object)} on these modules.
*
* @param testObject
* The test object, not null
*/
public void flushDatabaseUpdates(Object testObject) {
List flushables = Unitils.getInstance().getModulesRepository().getModulesOfType(Flushable.class);
for (Flushable flushable : flushables) {
flushable.flushDatabaseUpdates(testObject);
}
}
/**
* Updates the database version to the current version, without issuing any
* other updates to the database. This method can be used for example after
* you've manually brought the database to the latest version, but the
* database version is not yet set to the current one. This method can also
* be useful for example for reinitializing the database after having
* reorganized the scripts folder.
*
* @param sqlHandler
* The {@link DefaultSQLHandler} to which all commands are
* issued
*/
public void resetDatabaseState(SQLHandler sqlHandler, DataSourceWrapper wrapper) {
String schema = wrapper.databaseConfiguration.getDefaultSchemaName();
if (!StringUtils.isEmpty(schema)) {
DatabaseConfiguration databaseConfiguration = wrapper.getDatabaseConfiguration();
DBMaintainer dbMaintainer = new DBMaintainer(configuration, sqlHandler, databaseConfiguration.getDialect(), databaseConfiguration.getSchemaNames());
dbMaintainer.resetDatabaseState(schema, wrapper.getDatabaseConfiguration().isDefaultDatabase());
} else {
logger.debug("No schema found! The database is not reset!");
}
}
/**
* Assigns the TestDataSource
to every field annotated with
* {@link TestDataSource} and calls all methods annotated with
* {@link TestDataSource}
*
* @param testObject
* The test instance, not null
*/
public void injectDataSource(Object testObject) {
Set fields = getFieldsAnnotatedWith(testObject.getClass(), TestDataSource.class);
Set methods = getMethodsAnnotatedWith(testObject.getClass(), TestDataSource.class);
Map mapDatasources = new HashMap<>();
// update all databases
for (Entry wrapper : wrappers.entrySet()) {
DataSource dataSource2 = getDataSource(wrapper.getKey(), mapDatasources, testObject);
// look if datasource is needed in test.
setFieldDataSource(wrapper.getKey(), dataSource2, testObject, fields, methods);
}
}
protected void setFieldDataSource(String databaseName, DataSource dataSource, Object testObject, Set fields, Set methods) {
if (fields.isEmpty() && methods.isEmpty()) {
// Nothing to do. Jump out to make sure that we don't try to instantiate the DataSource
return;
}
for (Field field : fields) {
TestDataSource annotation = field.getAnnotation(TestDataSource.class);
if (annotation == null) {
continue;
}
String tempDatabaseName = StringUtils.isEmpty(annotation.value()) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName()
: annotation.value();
if (tempDatabaseName.equals(databaseName)) {
ReflectionUtils.setFieldValue(testObject, field, dataSource);
}
}
for (Method method : methods) {
TestDataSource annotation = method.getAnnotation(TestDataSource.class);
if (annotation == null) {
continue;
}
String tempDatabaseName = StringUtils.isEmpty(annotation.value()) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName()
: annotation.value();
if (tempDatabaseName.equals(databaseName)) {
try {
method.invoke(testObject, dataSource);
} catch (IllegalAccessException ex) {
logger.error(ex.getMessage(), ex);
} catch (IllegalArgumentException ex) {
logger.error(ex.getMessage(), ex);
} catch (InvocationTargetException ex) {
logger.error(ex.getMessage(), ex);
}
}
}
}
protected DataSource getDataSource(String databaseName, Map mapDatasources, Object testObject) {
DataSource datasource = null;
if (mapDatasources.containsKey(databaseName)) {
datasource = mapDatasources.get(databaseName);
} else {
DataSourceWrapper wrapper = getWrapper(databaseName);
datasource = wrapper.getTransactionalDataSourceAndActivateTransactionIfNeeded(testObject);
mapDatasources.put(databaseName, datasource);
}
return datasource;
}
/**
* @param testObject
* The test object, not null
* @param testMethod
* The test method, not null
* @return The {@link TransactionMode} for the given object
*/
protected TransactionMode getTransactionMode(Object testObject, Method testMethod) {
TransactionMode transactionMode = getMethodOrClassLevelAnnotationProperty(Transactional.class, "value", DEFAULT, testMethod, testObject.getClass());
transactionMode = getEnumValueReplaceDefault(Transactional.class, "value", transactionMode, defaultAnnotationPropertyValues);
return transactionMode;
}
/**
* Starts a transaction. If the Unitils DataSource was not loaded yet, we
* simply remember that a transaction was started but don't actually start
* it. If the DataSource is loaded within this test, the transaction will be
* started immediately after loading the DataSource.
*
* @param testObject
* The test object, not null
* @param testMethod
* The test method, not null
*/
protected void startTransactionForTestMethod(Object testObject, Method testMethod) {
if (isTransactionsEnabled(testObject, testMethod)) {
startTransaction(testObject);
}
}
/**
* Commits or rollbacks the current transaction, if transactions are enabled
* and a transactionManager is active for the given testObject
*
* @param testObject
* The test object, not null
* @param testMethod
* The test method, not null
*/
protected void endTransactionForTestMethod(Object testObject, Method testMethod) {
if (isTransactionsEnabled(testObject, testMethod)) {
if (getTransactionMode(testObject, testMethod) == COMMIT) {
commitTransaction(testObject);
} else if (getTransactionMode(testObject, testMethod) == ROLLBACK) {
rollbackTransaction(testObject);
}
}
}
/**
* Starts a new transaction on the transaction manager configured in unitils
*
* @param testObject
* The test object, not null
*/
public void startTransaction(Object testObject) {
getTransactionManager().startTransaction(testObject);
}
/**
* Commits the current transaction.
*
* @param testObject
* The test object, not null
*/
public void commitTransaction(Object testObject) {
flushDatabaseUpdates(testObject);
UnitilsTransactionManager transactionManager2 = getTransactionManager();
transactionManager2.activateTransactionIfNeeded(testObject);
transactionManager2.commit(testObject);
}
/**
* Performs a rollback of the current transaction
*
* @param testObject
* The test object, not null
*/
public void rollbackTransaction(Object testObject) {
flushDatabaseUpdates(testObject);
UnitilsTransactionManager transactionManager2 = getTransactionManager();
transactionManager2.activateTransactionIfNeeded(testObject);
transactionManager2.rollback(testObject);
}
/**
* @param testObject
* The test object, not null
* @param testMethod
* The test method, not null
* @return Whether transactions are enabled for the given test method and
* test object
*/
public boolean isTransactionsEnabled(Object testObject, Method testMethod) {
TransactionMode transactionMode = getTransactionMode(testObject, testMethod);
return transactionMode != DISABLED;
}
// todo javadoc
public void registerTransactionManagementConfiguration(UnitilsTransactionManagementConfiguration transactionManagementConfiguration) {
transactionManagementConfigurations.add(transactionManagementConfiguration);
}
protected Object getTestObject() {
return Unitils.getInstance().getTestContext().getTestObject();
}
/**
* @return The {@link TestListener} associated with this module
*/
@Override
public TestListener getTestListener() {
return new DatabaseTestListener();
}
/**
* The {@link TestListener} for this module
*/
protected class DatabaseTestListener
extends TestListener {
@Override
public void beforeTestSetUp(Object testObject, Method testMethod) {
List databaseNames = databaseConfigurations.getDatabaseNames();
if (!databaseNames.isEmpty()) {
for (String databaseName : databaseNames) {
DataSourceWrapper wrapper = getWrapper(databaseName);
setWrapper(wrapper);
}
} else {
// register default wrapper
getWrapper("");
}
try {
injectDataSource(testObject);
} catch (Exception e) {
throw new UnitilsException(e.getMessage(), e);
}
startTransactionForTestMethod(testObject, testMethod);
}
@Override
public void afterTestTearDown(Object testObject, Method testMethod) {
endTransactionForTestMethod(testObject, testMethod);
}
}
/**
* @return the wrapper
*/
public DataSourceWrapper getWrapper(String databaseName) {
String tempDatabaseName = StringUtils.isEmpty(databaseName) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName() : databaseName;
if (wrappers.containsKey(tempDatabaseName)) {
return wrappers.get(tempDatabaseName);
}
DataSourceWrapper wrapper = null;
if (StringUtils.isEmpty(databaseName)) {
wrapper = new DataSourceWrapper(databaseConfigurations.getDatabaseConfiguration(), configuration, getTransactionManager());
} else {
wrapper = new DataSourceWrapper(databaseConfigurations.getDatabaseConfiguration(databaseName), configuration, getTransactionManager());
}
setWrapper(wrapper);
return wrapper;
}
/**
* @param wrapper
* the wrapper to set
*/
public void setWrapper(DataSourceWrapper wrapper) {
if (!wrappers.keySet().contains(wrapper.getDatabaseName())) {
wrappers.put(wrapper.getDatabaseName(), wrapper);
registerTransactionManagementConfiguration(wrapper);
if (PropertyUtils.getBoolean(PROPERTY_RESET_SEQUENCES, false, configuration)) {
wrapper.restartSequences();
}
}
}
/**
* @return the databaseConfigurations
*/
public DatabaseConfigurations getDatabaseConfigurations() {
return databaseConfigurations;
}
}