Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

BeforeClass using Spring transactional tests

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.

like image 643
Adam B Avatar asked Jun 12 '09 20:06

Adam B


2 Answers

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

like image 103
Michael Pralow Avatar answered Sep 29 '22 12:09

Michael Pralow


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!

like image 30
Pablojim Avatar answered Sep 29 '22 10:09

Pablojim