EntityManagerFactory закрыт после перезагрузки контекста с помощью @DirtiesContext

У меня есть приложение Spring Boot, которое использует JMS для подключения к очереди и прослушивания входящих сообщений. В приложении у меня есть интеграционный тест, который отправляет некоторые сообщения в очередь, а затем проверяет, что вещи, которые должны произойти, когда слушатель получает новое сообщение, действительно происходят.

Я аннотировал свой тестовый класс с помощью @DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD) , чтобы убедиться, что моя база данных чиста после каждого теста. Каждый тест проходит успешно, если он выполняется изолированно. Однако при запуске их всех вместе после успешного прохождения первого теста следующий тест завершается сбоем, за исключением нижеприведенного исключения, когда тестируемый код пытается сохранить объект в базе данных:

    org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction; nested exception is java.lang.IllegalStateException: EntityManagerFactory is closed
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:431) ~[spring-orm-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:447) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:277) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at com.sun.proxy.$Proxy95.handleWorkflowEvent(Unknown Source) ~[na:na]
    at com.mottmac.processflow.infra.jms.EventListener.onWorkflowEvent(EventListener.java:51) ~[classes/:na]
    at com.mottmac.processflow.infra.jms.EventListener.onMessage(EventListener.java:61) ~[classes/:na]
    at org.apache.activemq.ActiveMQMessageConsumer.dispatch(ActiveMQMessageConsumer.java:1401) [activemq-client-5.14.3.jar:5.14.3]
    at org.apache.activemq.ActiveMQSessionExecutor.dispatch(ActiveMQSessionExecutor.java:131) [activemq-client-5.14.3.jar:5.14.3]
    at org.apache.activemq.ActiveMQSessionExecutor.iterate(ActiveMQSessionExecutor.java:202) [activemq-client-5.14.3.jar:5.14.3]
    at org.apache.activemq.thread.PooledTaskRunner.runTask(PooledTaskRunner.java:133) [activemq-client-5.14.3.jar:5.14.3]
    at org.apache.activemq.thread.PooledTaskRunner$1.run(PooledTaskRunner.java:48) [activemq-client-5.14.3.jar:5.14.3]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_77]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_77]
    at java.lang.Thread.run(Unknown Source) [na:1.8.0_77]
Caused by: java.lang.IllegalStateException: EntityManagerFactory is closed
    at org.hibernate.jpa.internal.EntityManagerFactoryImpl.validateNotClosed(EntityManagerFactoryImpl.java:367) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.jpa.internal.EntityManagerFactoryImpl.internalCreateEntityManager(EntityManagerFactoryImpl.java:316) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.jpa.internal.EntityManagerFactoryImpl.createEntityManager(EntityManagerFactoryImpl.java:286) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
    at org.springframework.orm.jpa.JpaTransactionManager.createEntityManagerForTransaction(JpaTransactionManager.java:449) ~[spring-orm-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:369) ~[spring-orm-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    ... 17 common frames omitted

Мой тестовый класс:

    @RunWith(SpringRunner.class)
@SpringBootTest(classes = { TestGovernance.class })
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
public class ActivitiIntegrationTest
{
    private static final String TEST_PROCESS_KEY = "oneTaskProcess";
    private static final String FIRST_TASK_KEY = "theTask";
    private static final String NEXT_TASK_KEY = "nextTask";

    @Autowired
    private JmsTemplate jms;

    @Autowired
    private WorkflowEventRepository eventRepository;

    @Autowired
    private TaskService taskService;

    @Test
    public void workFlowEventForRunningTaskMovesItToTheNextStage() throws InterruptedException
    {
        sendMessageToCreateNewInstanceOfProcess(TEST_PROCESS_KEY);

        Task activeTask = getActiveTask();        
        assertThat(activeTask.getTaskDefinitionKey(), is(FIRST_TASK_KEY));

        sendMessageToUpdateExistingTask(activeTask.getProcessInstanceId(), FIRST_TASK_KEY);

        Task nextTask = getActiveTask();        
        assertThat(nextTask.getTaskDefinitionKey(), is(NEXT_TASK_KEY));
    }

    @Test
    public void newWorkflowEventIsSavedToDatabaseAndKicksOffTask() throws InterruptedException
    {
        sendMessageToCreateNewInstanceOfProcess(TEST_PROCESS_KEY);

        assertThat(eventRepository.findAll(), hasSize(1));
    }

    @Test
    public void newWorkflowEventKicksOffTask() throws InterruptedException
    {
        sendMessageToCreateNewInstanceOfProcess(TEST_PROCESS_KEY);

        Task activeTask = getActiveTask();        
        assertThat(activeTask.getTaskDefinitionKey(), is(FIRST_TASK_KEY));
    }


    private void sendMessageToUpdateExistingTask(String processId, String event) throws InterruptedException
    {
        WorkflowEvent message = new WorkflowEvent();
        message.setRaisedDt(ZonedDateTime.now());
        message.setEvent(event);
        // Existing
        message.setIdWorkflowInstance(processId);
        jms.convertAndSend("workflow", message);
        Thread.sleep(5000);
    }

    private void sendMessageToCreateNewInstanceOfProcess(String event) throws InterruptedException
    {
        WorkflowEvent message = new WorkflowEvent();
        message.setRaisedDt(ZonedDateTime.now());
        message.setEvent(event);
        jms.convertAndSend("workflow", message);
        Thread.sleep(5000);
    }

    private Task getActiveTask()
    {
        // For some reason the tasks in the task service are hanging around even
        // though the context is being reloaded. This means we have to get the
        // ID of the only task in the database (since it has been cleaned
        // properly) and use it to look up the task.
        WorkflowEvent workflowEvent = eventRepository.findAll().get(0);
        Task activeTask = taskService.createTaskQuery().processInstanceId(workflowEvent.getIdWorkflowInstance().toString()).singleResult();
        return activeTask;
    }

}

Метод, который выдает исключение в приложении (repository — это просто стандартные данные Spring CrudRepository):

    @Override
    @Transactional
    public void handleWorkflowEvent(WorkflowEvent event)
    {
        try
        {
            logger.info("Handling workflow event[{}]", event);

            // Exception is thrown here:
            repository.save(event);

            logger.info("Saved event to the database [{}]", event);
            if(event.getIdWorkflowInstance() == null)
            {
                String newWorkflow = engine.newWorkflow(event.getEvent(), event.getVariables());
                event.setIdWorkflowInstance(newWorkflow);
            }
            else 
            {
                engine.moveToNextStage(event.getIdWorkflowInstance(), event.getEvent(), event.getVariables());
            }
        }
        catch (Exception e)
        {
            logger.error("Error while handling workflow event:" , e);
        }
    }

Мой тестовый класс конфигурации:

@SpringBootApplication
@EnableJms
@TestConfiguration
public class TestGovernance
{
    private static final String WORKFLOW_QUEUE_NAME = "workflow";

    @Bean
    public ConnectionFactory connectionFactory()
    {
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost?broker.persistent=false");
        return connectionFactory;
    }

    @Bean
    public EventListenerJmsConnection connection(ConnectionFactory connectionFactory) throws NamingException, JMSException
    {
        // Look up ConnectionFactory and Queue
        Destination destination = new ActiveMQQueue(WORKFLOW_QUEUE_NAME);

        // Create Connection
        Connection connection = connectionFactory.createConnection();

        Session listenerSession = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
        MessageConsumer receiver = listenerSession.createConsumer(destination);

        EventListenerJmsConnection eventListenerConfig = new EventListenerJmsConnection(receiver, connection);
        return eventListenerConfig;
    }
}

Слушатель сообщений JMS (не уверен, что это поможет):

/**
 * Provides an endpoint which will listen for new JMS messages carrying
 * {@link WorkflowEvent} objects.
 */
@Service
public class EventListener implements MessageListener
{
    Logger logger = LoggerFactory.getLogger(EventListener.class);

    private WorkflowEventHandler eventHandler;

    private MessageConverter messageConverter;

    private EventListenerJmsConnection listenerConnection;

    @Autowired
    public EventListener(EventListenerJmsConnection listenerConnection, WorkflowEventHandler eventHandler, MessageConverter messageConverter)
    {
        this.eventHandler = eventHandler;
        this.messageConverter = messageConverter;
        this.listenerConnection = listenerConnection;
    }

    @PostConstruct
    public void setUpConnection() throws NamingException, JMSException
    {
        listenerConnection.setMessageListener(this);
        listenerConnection.start();
    }

    private void onWorkflowEvent(WorkflowEvent event)
    {
        logger.info("Recieved new workflow event [{}]", event);
        eventHandler.handleWorkflowEvent(event);
    }

    @Override
    public void onMessage(Message message)
    {
        try
        {
            message.acknowledge();
            WorkflowEvent fromMessage = (WorkflowEvent) messageConverter.fromMessage(message);
            onWorkflowEvent((WorkflowEvent) fromMessage);
        }
        catch (Exception e)
        {
            logger.error("Error: ", e);
        }
    }
}

Я попытался добавить @Transactional' to the test methods and removing it from the code under test and various combinations with no success. I've also tried adding various test execution listeners and I still can't get it to work. If I remove the@DirtiesContext`, после чего исключение исчезло, и все тесты выполняются без исключений (однако они терпят неудачу с ошибками утверждения, как я и ожидал).

Любая помощь будет принята с благодарностью. Мои поиски пока ничего не дали, все говорит о том, что @DirtiesContext должен работать.


person Matt Watson    schedule 16.03.2017    source источник
comment
Это очень плохая причина для использования грязного контекста. Не делайте этого, это медленно, и когда ваш набор тестов вырастет, а количество bean-компонентов будет еще медленнее. Так что не надо. Сделайте свой тест @Transactional, и по умолчанию данные будут откатываться после вашего теста. Они могут потерпеть неудачу, поскольку ничего не зафиксировано, поэтому вам может понадобиться/хотеться внедрить EntityManager и поместить entityManager.flush() между вызовами методов для имитации фиксации. Вы даже используете SpringBootTest (только что заметил), что делает еще более ужасной идею перезапустить все приложение для теста.   -  person M. Deinum    schedule 16.03.2017
comment
Кроме того, я бы сказал, что ваша настройка JMS ошибочна, и вы можете сделать ее намного проще. Просто реализовав onMessage с @JmsLIstener и некоторыми именами очередей, Spring сделает все остальное.   -  person M. Deinum    schedule 16.03.2017
comment
Изначально у меня был @JmsListener, но что-то в том, как его настраивает автоматическая конфигурация, означало, что он не будет работать в производственной среде в сочетании с служебной шиной Microsoft.   -  person Matt Watson    schedule 16.03.2017


Ответы (1)


Использование @DirtiesContext для этого - ужасная идея (имхо), что вы должны сделать, это сделать свои тесты @Transactional. Я также предлагаю удалить Thread.sleep и вместо этого использовать что-то вроде awaitility.

Теоретически, когда вы выполняете запрос, все ожидающие изменения должны быть зафиксированы, поэтому вы можете использовать ожидание для проверки не более 6 секунд, чтобы увидеть, сохранилось ли что-то в базе данных. Если это не сработает, вы можете попробовать добавить флеш перед запросом.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { TestGovernance.class })
@Transactional
public class ActivitiIntegrationTest {

    private static final String TEST_PROCESS_KEY = "oneTaskProcess";
    private static final String FIRST_TASK_KEY = "theTask";
    private static final String NEXT_TASK_KEY = "nextTask";

    @Autowired
    private JmsTemplate jms;

    @Autowired
    private WorkflowEventRepository eventRepository;

    @Autowired
    private TaskService taskService;

    @Autowired
    private EntityManager em;

    @Test
    public void workFlowEventForRunningTaskMovesItToTheNextStage() throws InterruptedException
    {
        sendMessageToCreateNewInstanceOfProcess(TEST_PROCESS_KEY);

        await().atMost(6, SECONDS).until(getActiveTask() != null);

        Task activeTask = getActiveTask());
        assertThat(activeTask.getTaskDefinitionKey(), is(FIRST_TASK_KEY));

        sendMessageToUpdateExistingTask(activeTask.getProcessInstanceId(), FIRST_TASK_KEY);

        Task nextTask = getActiveTask();        
        assertThat(nextTask.getTaskDefinitionKey(), is(NEXT_TASK_KEY));
    }

    private Task getActiveTask()
    {
        em.flush(); // simulate a commit
        // For some reason the tasks in the task service are hanging around even
        // though the context is being reloaded. This means we have to get the
        // ID of the only task in the database (since it has been cleaned
        // properly) and use it to look up the task.
        WorkflowEvent workflowEvent = eventRepository.findAll().get(0);
        Task activeTask = taskService.createTaskQuery().processInstanceId(workflowEvent.getIdWorkflowInstance().toString()).singleResult();
        return activeTask;
    }

}

Возможно, вам понадобится / вы захотите немного отполировать свой getActiveTask, чтобы иметь возможность return null, или, может быть, это изменение заставляет его вести себя так, как вы ожидали.

Я только что сделал один метод, остальные вы, вероятно, можете понять сами. Ваш выигрыш при таком подходе, вероятно, будет в 2 раза больше: 1 он будет ждать не 5 секунд, а меньше, и вам не придется перезагружать все приложение между тестами. Оба из них должны сделать ваши тесты быстрее.

person M. Deinum    schedule 16.03.2017
comment
Я обновил тесты, чтобы использовать @Transactional, но, похоже, он не очищает базу данных. Я вижу в журнале отладки, что он что-то делает: 2017-03-16 19:24:11.216 DEBUG 12348 --- [ main] o.s.orm.jpa.JpaTransactionManager : Rolling back JPA transaction on EntityManager [org.hibernate.jpa.internal.EntityManagerImpl@18c820d2], но когда я запускаю все тесты вместе, он переходит к последнему тестовому методу, и в базе данных есть 4 элемента. Тест проходит изолированно, поэтому я знаю, что это определенно не тест, который помещает их все в базу данных. - person Matt Watson; 16.03.2017
comment
Я просматриваю журналы и задаюсь вопросом, имеет ли к этому какое-либо отношение тот факт, что тестируемый код выполняется в другом потоке. Обработка сообщений JMS выполняется в потоке [Session Task-1], а тесты выполняются в потоке [main]. Хотя кажется, что все происходит в правильном порядке. - person Matt Watson; 16.03.2017
comment
Это действительно то, что происходит, поскольку транзакции (и связанные с ними EntityManager) основаны на потоках, которые не будут работать. В чем, конечно же, вся идея JMS :). Я удалю ответ, так как в этом отношении он не имеет смысла (забыл о том, что вы на самом деле используете JMS в своих тестах). Тем не менее, я бы предложил использовать awaitility. - person M. Deinum; 17.03.2017
comment
Я переключился на awaitiliy сейчас. Я использовал его в других проектах раньше, Thread::sleep был просто быстрым и грязным исправлением, пока я не мог понять, что пошло не так. Я предполагаю, что единственное оставшееся решение - вручную очищать базу данных после каждого теста? - person Matt Watson; 17.03.2017
comment
Угадайте, что это самое быстрое, если вы действительно хотите уничтожить все, что вы можете обрезать все таблицы (что должно быть довольно быстро). - person M. Deinum; 17.03.2017
comment
Кажется, вставление eventRepository.deleteAll(); в блок @After помогает. - person Matt Watson; 17.03.2017