I'm using the Spring transactional test classes to do unit testing of my DAO code. What I want to do is create my database once, before all the tests run. I have a @BeforeClass annotated method but that runs before Spring loads up the application context and configures the jdbcTemplate, thus I don't actually have a connection to the DB at that time. Is there a way to run my DB setup once after the context loads but before the tests start running?
This thead asks the same question but the accepted solution seems to just be "don't do that". I'm inclined to say this just seems like it isn't doable.
my solution, a bit complicated but i needed it for an test framework :-) do not be afraid of the german javadocs, the method names and bodies should be enough to get it
FIRST create Annotation to mark Class or method for database work (create table and/or insert statements)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SchemaImport {
/**
* Location der Schemadatei(en). Die Datei darf nur SQL Statements enthalten.
* Wird keine Location gesetzt, greift der Defaultwert.
* @return String
*/
String[] locationsBefore() default {"input/schemas/before.sql"};
/**
* Location der Schemadatei(en). Die Datei darf nur SQL Statements enthalten.
* Wird keine Location gesetzt, greift der Defaultwert.
* @return String
*/
String[] locationsAfter() default {"input/schemas/after.sql"};
/**
* Ein SchemaImport findet nur bei passender Umgebungsvariable statt, mit diesem
* Flag kann dieses Verhalten geändert werden.
* @return boolean
*/
boolean override() default false;
}
SECOND create listener which looks for the annotation, AbstractTestExecutionListener is a Spring Framework Class from
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.test</artifactId>
<version>2.5.6</version>
</dependency>
public class SchemaImportTestExecutionListener extends AbstractTestExecutionListener
implements ApplicationContextAware {
/**
* Standard LOG Definition.
*/
private static final Logger LOG = LoggerFactory.getLogger(
SchemaImportTestExecutionListener.class);
/**
* Datasource Name - gemeint ist der Name der Datasource Bean bzw. die ID.
*/
private static final String DATASOURCE_NAME = "dataSource";
/**
* JDBC Template.
*/
private SimpleJdbcTemplate simpleJdbcTemplate;
/**
* Flag um festzustellen ob prepareTestInstance schon gerufen wurde.
*/
private boolean isAlreadyPrepared = false;
/**
* Standard Constructor, laut API von konkreten Implementierungen für
* TestexecutionListener erwartet, es geht aber auch ohne.
*/
public SchemaImportTestExecutionListener() {
}
/**
* Für jede Testklasse die mit der {@link SchemaImport} Annotation ausgezeichnet
* ist, wird ein entsprechender SchemaImport durchgeführt.
*
* Der SchemaImport findet pro Klasse exakt einmal statt. Diese Verhalten
* entspricht der BeforeClass
* Annotation von JUnit.
*
* Achtung mit Nutzung von Schemaimport auf Klassenebene ist kein
* Rollback möglich, stattdessen SchemaImport auf Methodenebene nutzen.
*
* @param testContext
* @throws java.lang.Exception
*/
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
final SchemaImport annotation = AnnotationUtils.findAnnotation(testContext.getTestClass(), SchemaImport.class);
if ((annotation != null) && !isAlreadyPrepared && (isPropertyOrOverride(annotation))) {
executeSchemaImports(testContext, annotation.locationsBefore(), true);
isAlreadyPrepared = true;
}
}
/**
* Für jede Testmethode mit {@link SchemaImport} werden die angegebenen
* Schema Dateien als SQL ausgeführt.
*
* @param testContext
* @throws java.lang.Exception
*/
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
// nur für Methoden mit passender Annotation Schemaimport durchführen
final SchemaImport annotation = AnnotationUtils.findAnnotation(testContext.getTestMethod(), SchemaImport.class);
if (annotation != null) {
executeSchemaImports(testContext, annotation.locationsBefore(), true);
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
// nur für Methoden mit passender Annotation Schemaimport durchführen
final SchemaImport annotation = AnnotationUtils.findAnnotation(testContext.getTestMethod(), SchemaImport.class);
if (annotation != null) {
executeSchemaImports(testContext, annotation.locationsAfter(), false);
}
}
/**
* Prüfen ob passende Umgebungsvariable gesetzt wurde. Diese kann durch
* entsprechendes Setzen des Flags an der Annotation überschrieben werden.
* @return
*/
private boolean isPropertyOrOverride(SchemaImport annotation) {
String prop = System.getProperty(TYPEnviroment.KEY_ENV);
if (StringUtils.trimToEmpty(prop).equals(TYPEnviroment.EMBEDDED.getEnv())) {
LOG.info("Running SchemaImport, Enviroment is set:'" + prop + "'");
return true;
} else {
if (annotation.override()) {
LOG.warn(
"Running SchemaImport, although Enviroment is set:'" + prop + "'");
return true;
} else {
LOG.warn(
"Not Running SchemaImport cause neither Environment or SchemaImport.override are set.");
return false;
}
}
}
/**
* Hilfesmethode die eigentlichen SchemaImport kapselt.
*
* @param testContext
* @param locations
*/
private void executeSchemaImports(TestContext testContext, String[] locations, boolean checkLocations) {
// für jede Datei SchemaImport durchführen, korrekte Reihenfolge
// ist durch Entwickler zu gewährleisten
if (locations.length > 0) {
for (String location : locations) {
if (StringUtils.trimToNull(location) != null) {
if (isResourceExistant(location, checkLocations)) {
LOG.info("Executing Schema Location: '" + location + "'");
SimpleJdbcTestUtils.executeSqlScript(getJdbcTemplate(
testContext), new ClassPathResource(location),
false);
} else {
LOG.warn(
"Schema Location '" + location + "' for SchemaImport not found.");
}
} else {
throw new RuntimeException("SchemaImport with empty Locations in:'" + testContext.getTestClass().getSimpleName() + "'");
}
}
}
}
/**
*
* @param resource
* @return
*/
private boolean isResourceExistant(String resource, boolean checkLocations) {
try {
new ClassPathResource(resource).getInputStream();
return true;
} catch (IOException ex) {
if (checkLocations) {
throw new RuntimeException(ex);
} else {
return false;
}
}
}
/**
* Hilfsmethode um an ein JdbcTemplate heranzukommen.
*
* @param TestContext
* @return SimpleJdbcTemplate
*/
private SimpleJdbcTemplate getJdbcTemplate(TestContext context) {
if (this.simpleJdbcTemplate == null) {
this.simpleJdbcTemplate = new SimpleJdbcTemplate(getDataSource(
context));
}
return this.simpleJdbcTemplate;
}
/**
* Hilfsmethode um an die Datasource heranzukommen.
*
* @param testContext
* @return DataSource
*/
private DataSource getDataSource(TestContext testContext) {
return (DataSource) testContext.getApplicationContext().getBean(
DATASOURCE_NAME, DataSource.class);
}
/** {@inheritDoc} */
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
throw new UnsupportedOperationException("Not supported yet.");
}
}
THIRD add the listener to the test execution
@ContextConfiguration(locations = {"classpath*:spring/persistence/*.xml"})
@Transactional
@TestExecutionListeners({
TransactionalTestExecutionListener.class,
SchemaImportTestExecutionListener.class})
public abstract class AbstractAvHibernateTests extends AbstractAvTests {
/**
* SimpleJdbcTemplate für Subclasses verfügbar.
*/
@Autowired
protected SimpleJdbcTemplate simpleJdbcTemplate;
}
in use
@SchemaImport(locationsBefore={"schemas/spring-batch/2.0.0/schema-hsqldb.sql"})
public class FooTest extends AbstractAvHibernateTests {
}
its important to note - beware of thread problems while using testNg parallel testing, for this to work there should be some 'synchronized' markers for the getJdbcTemplate / dataSource Methods in the listener
ps:
the code for the test base class:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:spring/*.xml"})
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
LogDurationTestExecutionListener.class,
LogMethodNameTestExecutionListener.class})
public abstract class AbstractAvTests implements ApplicationContextAware {
/**
* Logger für Subclasses verfügbar.
*/
protected final Logger LOG = LoggerFactory.getLogger(getClass());
/**
* {@link ApplicationContext} für Subclasses verfügbar.
*/
protected ApplicationContext applicationContext;
/** {@inheritDoc } */
@Override
public final void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
LogDurationTestExecutionListener and LogMethodNameTestExecutionListener are custom listeners not provided by spring, but not needed for the schemaImport to work properly
I'd second the advice that you should make each of your tests autonomous and therefore do all your setup with @Before rather than with @BeforeClass.
If you wish to stick with your approach just use the @Before method and have a simple boolean check to see if the setup has already been completed. e.g.
if(!databaseSetup) {
...set up the database
databaseSetup=true;
}
Not too fancy but it will work!
See my answer here for an example spring transaction test with annotations using dbunit.
Hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With