11

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.

Community
  • 1
  • 1
Adam B
  • 1,724
  • 3
  • 14
  • 29
  • What was your solution? How did you make it work? – Dave Jul 01 '10 at 00:59
  • I have since moved to Spring 3 and am using the jdbc:embedded-database xml tag to set up my database before the test runs. I also created a custom test execution listener that wraps DBUnit operations for validating/resetting the database state after each test case. See http://blog.zenika.com/index.php?post/2010/02/05/Testing-SQL-queries-with-Spring-and-DbUnit,-part-2 – Adam B Jul 01 '10 at 18:12
  • I just set it up, and BeforeClass annotated method runs after spring loads up the context..using testNG – lisak Jan 02 '11 at 02:24

4 Answers4

6

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

Michael Pralow
  • 6,560
  • 2
  • 30
  • 46
2

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!

Community
  • 1
  • 1
Pablojim
  • 8,542
  • 8
  • 45
  • 69
  • 7
    -1 because in fact this will NOT work because JUnit creates an instance of the test class for every method, thus your "databaseSetup" variable will always be false when entering the "@Before" method and you will still end up reinitializing the database before every test method. – Dave Jun 30 '10 at 21:58
  • 1
    That's true - thanks for pointing it out. I suppose you could use a static member variable but that's just getting deeper into the problem. – Pablojim Jul 01 '10 at 13:16
  • If you make you databaseSetup boolean static, this should work. – Lucas Mar 11 '11 at 17:47
1

I don't know what unit testing framework you are using but for JUnit, you can make your test class subclass AbstractTransactionalJUnit4SpringContextTests which has an executeSqlScript method this can either be run in a beforeclass or beforemethod method. My preference is to use BeforeMethod as this means that each of my unit tests are autonomous, even if it means my unit tests run a bit slower.

mR_fr0g
  • 8,462
  • 7
  • 39
  • 54
  • I wouldn't be able to put this in a @BeforeClass method because @BeforeClass methods must be static and are run before the application context is loaded. – Adam B Jun 13 '09 at 15:46
  • Ok, it is possible with TestNg if that is any help? – mR_fr0g Jun 13 '09 at 15:52
-4

Try using your old methods instead of the fancy annotations.

@BeforeClass
    public static void beforeClass() {
        ApplicationContext context = new ClassPathXmlApplicationContext(
        "applicationContext.xml");
                  [...]
    }