
com.edugility.liquiunit.DataSourceDatabaseTesterRule Maven / Gradle / Ivy
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
*
* Copyright (c) 2013-2014 Edugility LLC.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense and/or sell copies
* of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* The original copy of this license is available at
* http://www.opensource.org/license/mit-license.html.
*/
package com.edugility.liquiunit;
import java.io.Closeable;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.dbunit.AbstractDatabaseTester;
import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.DefaultOperationListener;
import org.dbunit.IDatabaseTester;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.datatype.IDataTypeFactory;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.xml.sax.InputSource;
/**
* An {@link ExternalResource} that wraps JUnit tests with a {@link
* DataSourceDatabaseTester} to ensure that the underlying database is
* populated appropriately with test data.
*
* @author Laird Nelson
*
* @see dbUnit
*
* @see DataSourceDatabaseTester
*/
public class DataSourceDatabaseTesterRule extends ExternalResource {
/*
* Instance fields.
*/
/**
* The underlying {@link DataSourceDatabaseTester} that forms the
* basis of this {@link DataSourceDatabaseTesterRule}.
*
* This field may be {@code null}.
*/
protected final DataSourceDatabaseTester tester;
/**
* The {@link Description} describing the current JUnit test
* underway.
*
* This field may be {@code null}.
*/
private Description description;
/**
* The {@link IDataSet} that may have previously been installed in
* the affiliated {@link #tester DataSourceDatabaseTester}.
*
* This field may be {@code null}.
*/
private IDataSet oldDataSet;
/**
* The {@link InputStream} used to {@linkplain #createDataSet(URL)
* logically open an IDataSet
}, stored here as a {@link
* Closeable}.
*
* This field may be {@code null}.
*
* @see #createDataSet(URL)
*
* @see #closeDataSet()
*/
private Closeable dataSetInputStream;
/*
* Constructors.
*/
/**
* Creates a {@link DataSourceDatabaseTesterRule}.
*
* @param dataSource the {@link DataSource} that will back a new
* {@link DataSourceDatabaseTester} {@linkplain
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource)
* created} by this constructor; must not be {@code null}
*
* @exception NullPointerException if {@code dataSource} is {@code
* null}
*
* @see DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource)
*/
public DataSourceDatabaseTesterRule(final DataSource dataSource) {
super();
this.tester = new DataSourceDatabaseTester(dataSource);
}
/**
* Creates a {@link DataSourceDatabaseTesterRule}.
*
* @param dataSource the {@link DataSource} that will back a new
* {@link DataSourceDatabaseTester} {@linkplain
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource)
* created} by this constructor; must not be {@code null}
*
* @param dataTypeFactory the {@link IDataTypeFactory} that
* describes data types for the database to which the supplied
* {@link DataSource} is notionally connected; may be {@code null}
*
* @exception NullPointerException if {@code dataSource} is {@code
* null}
*
* @see DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource)
*/
public DataSourceDatabaseTesterRule(final DataSource dataSource, final IDataTypeFactory dataTypeFactory) {
this(dataSource);
this.installDataTypeFactory(dataTypeFactory);
}
/**
* Creates a {@link DataSourceDatabaseTesterRule}.
*
* @param dataSource the {@link DataSource} that will back a new
* {@link DataSourceDatabaseTester} {@linkplain
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String) created} by this constructor; must not be {@code null}
*
* @param schema the parameter value to supply to the {@link
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String)} constructor; may be {@code null}
*
* @see
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String)
*/
public DataSourceDatabaseTesterRule(final DataSource dataSource, final String schema) {
super();
this.tester = new DataSourceDatabaseTester(dataSource, schema);
}
/**
* Creates a {@link DataSourceDatabaseTesterRule}.
*
* @param dataSource the {@link DataSource} that will back a new
* {@link DataSourceDatabaseTester} {@linkplain
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String) created} by this constructor; must not be {@code null}
*
* @param schema the parameter value to supply to the {@link
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String)} constructor; may be {@code null}
*
* @param dataTypeFactory the {@link IDataTypeFactory} that
* describes data types for the database to which the supplied
* {@link DataSource} is notionally connected; may be {@code null}
*
* @see
* DataSourceDatabaseTester#DataSourceDatabaseTester(DataSource,
* String)
*/
public DataSourceDatabaseTesterRule(final DataSource dataSource, final String schema, final IDataTypeFactory dataTypeFactory) {
this(dataSource, schema);
this.installDataTypeFactory(dataTypeFactory);
}
/**
* Creates a {@link DataSourceDatabaseTesterRule}.
*
* This constructor assumes that the supplied {@link
* DataSourceDatabaseTester} is completely configured and this
* {@link DataSourceDatabaseTesterRule} will therefore perform no
* further configuration. Please see in particular the {@link
* #getDataSet(Description)} method, which will consequently have no
* effect.
*
* @param tester the {@link DataSourceDatabaseTester} to which most
* operations will delegate; must not be {@code null}
*
* @exception IllegalArgumentException if {@code tester} is {@code
* null}
*/
public DataSourceDatabaseTesterRule(final DataSourceDatabaseTester tester) {
super();
if (tester == null) {
throw new IllegalArgumentException("tester", new NullPointerException("tester"));
}
this.tester = tester;
}
/*
* Instance methods.
*/
/**
* Ensures that every {@link IDatabaseConnection} produced during
* the course of execution is configured to use the supplied {@link
* IDataTypeFactory}.
*
* @param dataTypeFactory the {@link IDataTypeFactory} suitable for
* the underlying database; may be {@code null}
*
* @see IOperationListener#connectionRetrieved(IDatabaseConnection)
*
* @see DatabaseConfig#PROPERTY_DATATYPE_FACTORY
*/
private final void installDataTypeFactory(final IDataTypeFactory dataTypeFactory) {
if (dataTypeFactory != null && this.tester != null) {
this.tester.setOperationListener(new DefaultOperationListener() {
@Override
public final void connectionRetrieved(final IDatabaseConnection connection) {
super.connectionRetrieved(connection);
if (connection != null) {
final DatabaseConfig config = connection.getConfig();
if (config != null) {
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, dataTypeFactory);
}
}
}
});
}
}
/**
* {@linkplain #getDataSet(Description) Finds an appropriate
* IDataSet
} for the {@linkplain #tester affiliated
* DataSourceDatabaseTester
} and {@linkplain
* AbstractDatabaseTester#setDataSet(IDataSet) installs it}
* immediately before invoking the {@link IDatabaseTester#onSetup()}
* method.
*
* @exception Exception if an error occurs
*/
@Override
public void before() throws Exception {
if (this.tester != null) {
final IDataSet oldDataSet = this.tester.getDataSet();
this.oldDataSet = oldDataSet;
if (oldDataSet == null) {
IDataSet newDataSet = this.getDataSet(this.description);
if (newDataSet == null) {
newDataSet = new DefaultDataSet();
}
this.tester.setDataSet(newDataSet);
}
this.tester.onSetup();
}
}
/**
* Invokes the {@link IDatabaseTester#onTearDown()} method.
*
* @see IDatabaseTester#onTearDown()
*/
@Override
public void after() {
if (this.tester != null) {
try {
this.tester.onTearDown();
} catch (final RuntimeException throwMe) {
throw throwMe;
} catch (final Exception everythingElse) {
throw new RuntimeException(everythingElse); // TODO: ugly
} finally {
try {
this.closeDataSet();
} catch (final IOException ignore) {
}
}
this.tester.setDataSet(this.oldDataSet);
}
this.description = null; // XXX TODO INVESTIGATE: not sure this is proper
}
/**
* Overrides the {@link ExternalResource#apply(Statement,
* Description)} method to store the supplied {@link Description}
* for usage by the {@link #before()} method internally and returns
* the superclass' return value.
*
* It must be assumed that this method may return {@code null}
* since the superclass documentation does not mention whether the
* return value must be non-{@code null}.
*
* @param base the {@link Statement} to decorate; the superclass
* documentation does not define what behavior will occur if this
* parameter is {@code null}
*
* @param description the {@link Description} describing the test
* underway; the superclass documentation does not define what
* behavior will occur if this parameter is {@code null}
*
* @return a {@link Statement}; possibly {@code null}
*
* @see #before()
*
* @see ExternalResource#apply(Statement, Description)
*/
@Override
public Statement apply(final Statement base, final Description description) {
this.description = description;
return super.apply(base, description);
}
/**
* Follows various conventions, detailed below, in attempting to
* locate and instantiate a new {@link IDataSet} instance
* appropriate for the supplied {@link Description}.
*
* This implementation first checks the {@link #tester
* DataSourceDatabaseTester} indirectly or directly supplied to this
* {@link DataSourceDatabaseTesterRule} at {@linkplain
* #DataSourceDatabaseTesterRule(DataSource) construction time} to
* see if {@linkplain DataSourceDatabaseTester#getDataSet() it
* already has a IDataSet
implementation installed}.
* If so, then no further action is taken and that {@link IDataSet}
* is returned. ({@code null} is returned if the supplied {@code
* description} is {@code null}.)
*
* Otherwise, a {@linkplain ClassLoader#getResource(String)
* classpath resource} named {@code
* datasets/SIMPLE_TEST_CLASS_NAME/TEST_METHOD_NAME.xml} is sought
* using the {@linkplain Thread#getContextClassLoader() context
* classloader}, where {@code SIMPLE_TEST_CLASS_NAME} is the name of
* the JUnit test {@link Class} currently running and {@code
* TEST_METHOD_NAME} is the name of the JUnit test method currently
* running.
*
* If that resource doesn't exist, then a {@linkplain
* ClassLoader#getResource(String) classpath resource} named {@code
* datasets/SIMPLE_TEST_CLASS_NAME.xml} is sought using the
* {@linkplain Thread#getContextClassLoader() context classloader},
* where {@code SIMPLE_TEST_CLASS_NAME} is the name of the JUnit
* test {@link Class} currently running.
*
* Once a resource is located in this manner, its {@link URL} is
* passed to the {@link #createDataSet(URL)} method, and that
* method's return value is returned.
*
* If no resource exists, then {@code null} is returned.
*
* @param description a {@link Description} describing the JUnit
* test being executed; may be {@code null}
*
* @return an {@link IDataSet} instance appropriate for the supplied
* {@link Description}, or {@code null}
*
* @exception DataSetException if there was an error in constructing
* the {@link IDataSet}
*
* @exception IOException if there was an input/output error
*
* @see #createDataSet(URL)
*/
protected IDataSet getDataSet(final Description description) throws DataSetException, IOException {
final IDataSet returnValue;
if (this.tester == null) {
returnValue = null;
} else {
final IDataSet old = this.tester.getDataSet();
if (old == null && description != null) {
final String simpleClassName;
final Class> testClass = description.getTestClass();
if (testClass == null) {
simpleClassName = null;
} else {
simpleClassName = testClass.getSimpleName();
}
final String methodName = description.getMethodName();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = ClassLoader.getSystemClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
}
assert cl != null;
URL url = cl.getResource(String.format("datasets/%s/%s.xml", simpleClassName, methodName));
if (url == null) {
url = cl.getResource(String.format("datasets/%s.xml", simpleClassName));
}
returnValue = this.createDataSet(url);
} else {
returnValue = old;
}
}
return returnValue;
}
/**
* Creates a new {@link IDataSet} implementation suitable for the
* supplied {@link URL} and returns it.
*
* This method may return {@code null}.
*
* Overrides of this method are permitted to return {@code
* null}.
*
* This implementation returns {@code null} if a {@code null}
* {@link URL} is supplied. Otherwise, a new {@link XmlDataSet} is
* constructed with the {@linkplain URL#openStream() supplied
* URL
's affiliated InputStream
} and
* returned.
*
* @param url the {@link URL} for which a new {@link IDataSet}
* implementation should be returned; may be {@code null}
*
* @return a new {@link IDataSet} implementation, or {@code null}
*
* @exception DataSetException if an error occurs during {@link
* IDataSet} construction
*
* @exception IOException if an error occurs during the processing
* of the supplied {@link URL}
*
* @see #getDataSet(Description)
*/
protected IDataSet createDataSet(final URL url) throws DataSetException, IOException {
final IDataSet returnValue;
if (url == null) {
returnValue = null;
} else {
final InputStream stream = url.openStream();
this.dataSetInputStream = stream;
returnValue = new XmlDataSet(stream);
}
return returnValue;
}
/**
* Notionally closes any resources held by the {@link IDataSet}
* created by the {@link #createDataSet(URL)} method.
*
* @exception IOException if an error occurs during closing
*
* @see #createDataSet(URL)
*/
protected void closeDataSet() throws IOException {
if (this.dataSetInputStream != null) {
this.dataSetInputStream.close();
}
}
}