为什么新线程在事务性Spring JUnit测试中看不到主线程准备的测试数据?

时间:2019-02-15 02:23:13

标签: java spring spring-boot junit spring-test

我用Spring-boot-test编写了Junit测试,在一种测试方法中,我首先准备了一些应保存到MySQL DB的测试数据,然后调用了应该在100个子测试中测试的目标方法。测试目标方法在并发中是否运作良好的线程。此测试方法如下所示:

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();
    }

BaseServiceTest看起来像这样:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
@Transactional
@Rollback
public class BaseServiceTest {
    protected  Logger logger = LoggerFactory.getLogger(BaseServiceTest.class);
}

目标方法的签名如下:

public synchronized String createSheetIdWithoutUniformedSerial(String codeRuleNo, String fieldValuesJson)

在目标方法中,它查询由“准备测试数据”代码块保存的数据,然后执行一些业务逻辑代码,然后返回结果。 顺便说一下,写在“业务服务层”中的目标方法具有由Spring AOP管理的事务,并且事务配置文件如下所示:

<?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>

我的预期结果是同步化的目标方法效果很好

但是!在每个子任务线程中,目标方法无法查询出在主线程中准备好的测试数据!

所以,我很困惑,无法弄清问题出在哪里?需要帮助或提示,我非常感谢你们的帮助,谢谢!

ps:spring-boot版本是:1.5.10.RELEASE,Juite版本是:4.12

1 个答案:

答案 0 :(得分:5)

通过您的SysCodeRuleService与数据库的初始交互会将未提交给数据库的测试数据插入到测试管理的事务中。

在您的createSheetIdWithoutUniformedSerial()上对SysCodeRuleService的调用在新线程中执行;但是,Spring不会将事务传播到新产生的线程。因此,createSheetIdWithoutUniformedSerial()的调用在另一个线程中运行,该线程无法在挂起的测试托管事务中看到未提交的测试数据。

为了允许createSheetIdWithoutUniformedSerial()在新线程中查看数据库中的此类测试数据,必须在生成任何新线程之前将测试数据提交至数据库。

有几种方法可以实现这一目标。

如果您正在寻找一种非常低级的技术,则可以使用Spring's TransactionTemplate以编程方式提交到数据库。这甚至可以与已进行测试管理的事务一起使用(即通过测试类或测试方法上的@Transactional)。

如果您想执行特定于当前@Test方法的数据库设置,另一种选择是使用TestTransaction API。有关详细信息,请参见《 Spring Framework参考手册》中的Programmatic Transaction Management

如果要对当前类中的所有测试方法执行相同的数据库设置,则可以引入将测试数据插入数据库的@BeforeTransaction方法,并删除删除数据库中的@AfterTransaction方法。从数据库测试数据。参见Running Code Outside of a Transaction

如果您愿意或有兴趣将测试数据设置转移到SQL插入语句(可能在外部文件中),则可以使用Spring's @Sql support

请注意,由于可以有效地使用默认语义覆盖默认语义,因此可以安全地删除@Rollback声明。