I wrote a Junit test with The Spring-boot-test, in a test method, i firstly prepared some test data which should be saved to the MySQL DB, then i invoked the target method which should be tested in 100 sub-threads to test whether target method works well in concurrency . This test method looks like this:
public class SysCodeRuleServiceImplTest extends BaseServiceTest {
@Autowired
private SysCodeRuleService sysCodeRuleService;
@Autowired
private SysCodeRuleDtlService sysCodeRuleDtlService;
private final String codeRuleNo = "sdkfjks443";
@Test
public void testCreateSheetIdWithoutUniformedSerial_2() throws InterruptedException {
//------ prepare test data start-----------
SysCodeRule sysCodeRule = new SysCodeRule();
sysCodeRule.setCodeRuleNo(codeRuleNo);
sysCodeRule.setIfDateCode(1);
sysCodeRule.setPadChar("0");
sysCodeRule.setSerialDigits(6);
sysCodeRule.setResetMode(1);
sysCodeRule.setIfUniteSerial(0);
sysCodeRule.setIfCache(0);
sysCodeRule.setConstValue("PETREL");
sysCodeRule.setStatus(1);
sysCodeRule.setName(codeRuleNo);
sysCodeRule.setCurSerialNo("0");
sysCodeRule.setCurSerialDate(new Date());
sysCodeRule.setCreateTime(new Date());
sysCodeRule.setCreator("自动");
sysCodeRule.setDateCutBeginPosition(3);
sysCodeRule.setDateCutEndPosition(8);
boolean insertSysCodeRuleSucc = sysCodeRuleService.insert(sysCodeRule);
assertThat(TestMessageConstants.PREPARE_TEST_DATA_FAILED, insertSysCodeRuleSucc);
assertThat("", sysCodeRule.getId(), notNullValue());
SysCodeRuleDtl sysCodeRuleDtl1 = new SysCodeRuleDtl();
sysCodeRuleDtl1.setSysCodeId(sysCodeRule.getId() + "");
sysCodeRuleDtl1.setOrderNo(1);
sysCodeRuleDtl1.setFieldValue("locno");
sysCodeRuleDtl1.setCutEndPosition(0);
sysCodeRuleDtl1.setCutBeginPosition(0);
sysCodeRuleDtl1.setCreateTime(new Date());
sysCodeRuleDtl1.setCreator("自动");
boolean insertDtl1Succ = sysCodeRuleDtlService.insert(sysCodeRuleDtl1);
assertThat("", insertDtl1Succ);
SysCodeRuleDtl sysCodeRuleDtl2 = new SysCodeRuleDtl();
sysCodeRuleDtl2.setSysCodeId(sysCodeRule.getId() + "");
sysCodeRuleDtl2.setOrderNo(2);
sysCodeRuleDtl2.setFieldValue("fieldName1");
sysCodeRuleDtl2.setCutBeginPosition(1);
sysCodeRuleDtl2.setCutEndPosition(3);
sysCodeRuleDtl2.setCreateTime(new Date());
sysCodeRuleDtl2.setCreator("自动");
boolean insertDtl1Succ2 = sysCodeRuleDtlService.insert(sysCodeRuleDtl2);
assertThat("", insertDtl1Succ2);
//------prepare test data end------------------------
//startLatch used to make sure all task threads start after
//prepared test data done
CountDownLatch startLatch = new CountDownLatch(1);
//parameters needed by the target method
Map<String, String> fieldValueMap = new HashMap<>(2);
fieldValueMap.put("locno", "cangku1");
fieldValueMap.put("fieldName1", "ABCDEFGH");
//doneLatch used to make sure all task threads done before the
//the transaction which started in main thread roll back
CountDownLatch doneLatch = new CountDownLatch(100);
for(int i = 0; i < 100; i++) {
new Thread(() -> {
try {
startLatch.await();
//this is the target method which i want to test
String result = sysCodeRuleService.createSheetIdWithoutUniformedSerial(codeRuleNo, JsonUtils.writeValueAsString(fieldValueMap));
if(CommonUtil.isNotNull(result)) {
logger.debug(">>>>>>>>>>>>>" + result);
}
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
doneLatch.countDown();
}
}).start();
}
//guarantee the test data truly saved before all task treads
//start
EntityWrapper<SysCodeRule> ew = new EntityWrapper<>();
ew.eq("code_rule_no", codeRuleNo);
SysCodeRule codeRule = sysCodeRuleService.selectOne(ew);
if(codeRule != null) {
startLatch.countDown();
}
//main thread keep waiting until all task threads done their
//work
doneLatch.await();
}
The BaseServiceTest
looks like this :
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
@Transactional
@Rollback
public class BaseServiceTest {
protected Logger logger = LoggerFactory.getLogger(BaseServiceTest.class);
}
And the target method's signature looks like this:
public synchronized String createSheetIdWithoutUniformedSerial(String codeRuleNo, String fieldValuesJson)
in the target method, it query the data which saved by the "prepare test data" codes block then do some business logic codes then return the result. By the way, the target method which wrote in the "Business Service Layer" has transaction managed by Spring AOP, and the transaction config file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="doReweight" propagation="REQUIRES_NEW"/>
<tx:method name="doClear*" propagation="REQUIRES_NEW"/>
<tx:method name="doSend*" propagation="REQUIRES_NEW"/>
<tx:method name="doBatchSave*" propagation="REQUIRES_NEW"/>
<tx:method name="get*" propagation="REQUIRED" read-only="true"/>
<tx:method name="count*" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" propagation="REQUIRED" read-only="true"/>
<tx:method name="list*" propagation="REQUIRED" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config expose-proxy="true" proxy-target-class="true">
<aop:pointcut id="txPointcut"
expression="execution(* com.xxx..service..*+.*(..))"/>
<aop:advisor id="txAdvisor" advice-ref="txAdvice"
pointcut-ref="txPointcut"/>
</aop:config>
</beans>
My expected result is the syncronized target method works well
But! In each sub task thread, the target method can not query out the prepared test data which prepared in the main thread!
So, i was confused, and can not figure it out what was wrong?Need some help or tips, I really appreciate some help from you guys, thanks in advance!
ps: the spring-boot verison is:1.5.10.RELEASE, the Juite Version is:4.12
The initial interaction with the database via your SysCodeRuleService
inserts test data within the test-managed transaction which is not committed to the database.
The invocation of createSheetIdWithoutUniformedSerial()
on your SysCodeRuleService
then executes in a new thread; however, Spring does not propagate transactions to newly spawned threads. Thus, the invocation of createSheetIdWithoutUniformedSerial()
runs in a different thread which cannot see the uncommitted test data in the suspended test-managed transaction.
In order to allow createSheetIdWithoutUniformedSerial()
to see such test data in the database in a new thread, you will have to commit the test data to the database before spawning any new threads.
There are several options for achieving this.
If you're looking for a very low-level technique, you can use Spring's TransactionTemplate
to programmatically commit to the database. This should even work with a test-managed transaction in place (i.e., via @Transactional
on the test class or test method).
If you would like to perform database setup specific to the current @Test
method, another option is to use the TestTransaction
API. See Programmatic Transaction Management in the Spring Framework Reference Manual for details.
If you want to perform the same database setup for all test methods in the current class, you can introduce a @BeforeTransaction
method that inserts the test data into the database and an @AfterTransaction
method that deletes the test data from the database. See Running Code Outside of a Transaction.
If you are willing to or interested in moving your test data setup to SQL insert statements (potentially in an external file), you can use Spring's @Sql support.
As a side note, you can safely remove the @Rollback
declaration since you are effectively overriding the default semantics with the default semantics.