java - Spring transactions in unit tests and annotation based configuration -
i have trouble getting transactions working in unit test. transactiontest class contains necessary spring configuration. starts up, initializes database , executes 2 runnables in parallel (inserter , selector). logged output visible test executes, records inserted , selected database in correct order, there no transaction isolation.
what expect see in log like:
2016-01-16 00:29:32,447 [main] debug transactiontest - starting test 2016-01-16 00:29:32,619 [pool-2-thread-2] debug selector - select 1 returned: 0 2016-01-16 00:29:33,121 [pool-2-thread-1] debug inserter - inserting record: 1 2016-01-16 00:29:33,621 [pool-2-thread-2] debug selector - select 2 returned: 0 2016-01-16 00:29:34,151 [pool-2-thread-1] debug inserter - inserting record: 2 2016-01-16 00:29:34,624 [pool-2-thread-2] debug selector - select 3 returned: 2 2016-01-16 00:29:34,624 [main] debug transactiontest - terminated
however, see is:
2016-01-16 00:29:32,447 [main] debug transactiontest - starting test 2016-01-16 00:29:32,619 [pool-2-thread-2] debug selector - select 1 returned: 0 2016-01-16 00:29:33,121 [pool-2-thread-1] debug inserter - inserting record: 1 2016-01-16 00:29:33,621 [pool-2-thread-2] debug selector - select 2 returned: 1 2016-01-16 00:29:34,151 [pool-2-thread-1] debug inserter - inserting record: 2 2016-01-16 00:29:34,624 [pool-2-thread-2] debug selector - select 3 returned: 2 2016-01-16 00:29:34,624 [main] debug transactiontest - terminated
please consider test code below. in transactiontest.java there few annotations commented out ahead of class body itself. when include these annotations, can see log spring executes entire test in separate transaction. aim execute method inserter.insertseveralrecords() in separate transaction. there sadly no indications in log spring sees @transactional annotation there.
i have tried add @enabletransactionmanagement annotation transactiontest class itself, instead of configuration section, makes no difference.
transactiontest.java
package program.test.db.transaction; import java.util.concurrent.executors; import java.util.concurrent.threadpoolexecutor; import java.util.concurrent.timeunit; import org.apache.commons.dbcp2.basicdatasource; import org.apache.logging.log4j.logmanager; import org.apache.logging.log4j.logger; import org.flywaydb.core.flyway; import org.flywaydb.test.annotation.flywaytest; import org.flywaydb.test.junit.flywaytestexecutionlistener; import org.jooq.dslcontext; import org.jooq.sqldialect; import org.jooq.impl.datasourceconnectionprovider; import org.jooq.impl.defaultconfiguration; import org.jooq.impl.defaultdslcontext; import org.jooq.impl.defaultexecutelistenerprovider; import org.junit.test; import org.junit.runner.runwith; import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.context.annotation.propertysource; import org.springframework.core.env.environment; import org.springframework.jdbc.datasource.datasourcetransactionmanager; import org.springframework.jdbc.datasource.transactionawaredatasourceproxy; import org.springframework.test.context.contextconfiguration; import org.springframework.test.context.testexecutionlisteners; import org.springframework.test.context.junit4.springjunit4classrunner; import org.springframework.test.context.support.annotationconfigcontextloader; import org.springframework.test.context.support.dependencyinjectiontestexecutionlistener; import org.springframework.test.context.transaction.transactionconfiguration; import org.springframework.test.context.transaction.transactionaltestexecutionlistener; import org.springframework.transaction.annotation.enabletransactionmanagement; import org.springframework.transaction.annotation.transactional; import program.db.jooqexceptiontranslator; import static org.junit.assert.asserttrue; import static program.db.tables.system_log; @runwith(springjunit4classrunner.class) @contextconfiguration(loader=annotationconfigcontextloader.class) @testexecutionlisteners({dependencyinjectiontestexecutionlistener.class, flywaytestexecutionlistener.class})//, transactionaltestexecutionlistener.class}) //@transactional //@transactionconfiguration(transactionmanager="transactionmanager", defaultrollback=false) public class transactiontest { private static logger log = logmanager.getlogger(transactiontest.class); @configuration @propertysource("classpath:program.properties") @enabletransactionmanagement static class contextconfiguration { @autowired private environment env; @bean public flyway flyway(){ flyway flyway = new flyway(); flyway.setdatasource(datasource()); flyway.setschemas("program_x"); flyway.setlocations("db/migration"); return flyway; } @bean public basicdatasource datasource() { basicdatasource result = new basicdatasource(); result.setdriverclassname(env.getrequiredproperty("program.database.driver")); result.seturl(env.getrequiredproperty("program.database.url")); result.setusername(env.getrequiredproperty("program.database.username")); result.setpassword(env.getrequiredproperty("program.database.password")); return result; } @bean public datasourcetransactionmanager transactionmanager() { return new datasourcetransactionmanager(datasource()); } @bean public transactionawaredatasourceproxy transactionawaredatasource(){ return new transactionawaredatasourceproxy(datasource()); } @bean public datasourceconnectionprovider connectionprovider(){ return new datasourceconnectionprovider(transactionawaredatasource()); } @bean public jooqexceptiontranslator jooqexceptiontranslator(){ return new jooqexceptiontranslator(); } @bean public defaultconfiguration config(){ defaultconfiguration result = new defaultconfiguration(); result.set(connectionprovider()); result.set(new defaultexecutelistenerprovider(jooqexceptiontranslator())); result.set(sqldialect.postgres); return result; } @bean public defaultdslcontext db(){ return new defaultdslcontext(config()); } @bean public inserter inserter(){ return new inserter(); } @bean public selector selector(){ return new selector(); } } @autowired private dslcontext db; @autowired private selector selector; @autowired private inserter inserter; private final threadpoolexecutor thread_pool = (threadpoolexecutor) executors.newcachedthreadpool(); @test @flywaytest public void runtest() throws interruptedexception { log.debug("starting test"); int count0 = db.selectcount().from(system_log).fetchone(0, int.class); asserttrue(count0 == 0); thread_pool.execute(inserter); thread_pool.execute(selector); thread_pool.shutdown(); thread_pool.awaittermination(5, timeunit.seconds); log.debug("terminated"); } }
selector.java
package program.test.db.transaction; import org.apache.logging.log4j.logmanager; import org.apache.logging.log4j.logger; import org.jooq.dslcontext; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; import static program.db.tables.system_log; @component public class selector implements runnable { private static logger log = logmanager.getlogger(selector.class); @autowired private dslcontext db; @override public void run() { try { int count1 = db.selectcount().from(system_log).fetchone(0, int.class); log.debug("select 1 returned: " + count1); thread.sleep(1000); int count2 = db.selectcount().from(system_log).fetchone(0, int.class); log.debug("select 2 returned: " + count2); thread.sleep(1000); int count3 = db.selectcount().from(system_log).fetchone(0, int.class); log.debug("select 3 returned: " + count3); } catch (interruptedexception e) { log.error("selects interrupted", e); } } }
inserter.java
package program.test.db.transaction; import org.apache.logging.log4j.logmanager; import org.apache.logging.log4j.logger; import org.joda.time.datetime; import org.jooq.dslcontext; import org.jooq.insertquery; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; import org.springframework.transaction.annotation.transactional; import program.db.tables.records.systemlogrecord; import static org.junit.assert.asserttrue; import static program.db.tables.system_log; @component public class inserter implements runnable { private static logger log = logmanager.getlogger(inserter.class); @autowired private dslcontext db; @override public void run() { insertseveralrecords(); } @transactional private void insertseveralrecords(){ try { thread.sleep(500); insertrecord(1); thread.sleep(1000); insertrecord(2); } catch (interruptedexception e) { log.error("inserts interrupted", e); } } private void insertrecord(int i){ log.debug("inserting record: " + i); insertquery<systemlogrecord> insertquery = db.insertquery(system_log); insertquery.addvalue(system_log.service, "service " + i); insertquery.addvalue(system_log.message, "message " + i); insertquery.addvalue(system_log.sys_insert_time, datetime.now()); int result = insertquery.execute(); asserttrue(result == 1); } }
i missing rather basic here - doing wrong?
the problem in question caused by:
the method inserter.insertseveralrecords() annotated @transactional private method.
- only public methods should annotated @transactional
making method inserter.insertseveralrecords() public still did not start transaction. because method called internally method inserter.run() (as opposed externally other class).
- when adding support @transactional, spring uses proxies add code before , after invocation of annotated method. in case of classes implement interface, these dynamic proxies. means external method calls come in through proxy intercepted
- the class inserter implements runnable interface - @transactional picked if annotated method called directly outside
moving @transactional annotation method inserter.run() fixes class still not enough run test. when starting up, throw error:
"could not autowire field: transactiontest.inserter; nosuchbeandefinitionexception: no qualifying bean of type [program.test.db.transaction.inserter] found dependency"
this caused fact transactiontest.inserter field of type inserter , not runnable, whereas @transactional annotation added method of runnable interface. not find reference why works this, changing @autowired field type inserter runnable allows spring start correctly , use transactions when executer calls inserter.run(). (probably because dynamic proxy created on interface also?)
here relevant code sections 3 changes described above:
inserter.java
@override @transactional public void run() { insertseveralrecords(); } private void insertseveralrecords(){ try { thread.sleep(500); insertrecord(1); thread.sleep(1000); insertrecord(2); } catch (interruptedexception e) { log.error("inserts interrupted", e); } }
transactiontest.java
@autowired private dslcontext db; @autowired private runnable selector; @autowired private runnable inserter; private final threadpoolexecutor thread_pool = (threadpoolexecutor) executors.newcachedthreadpool(); @test @flywaytest public void runtest() throws interruptedexception { log.debug("starting test"); int count0 = db.selectcount().from(system_log).fetchone(0, int.class); asserttrue(count0 == 0); thread_pool.execute(inserter); thread_pool.execute(selector); thread_pool.shutdown(); thread_pool.awaittermination(5, timeunit.seconds); log.debug("terminated"); }
the test executes correctly transactional isolation, producing log output desired in original question.
used resources:
Comments
Post a Comment