Тесты не выполняются с @Scheduled Task: JdbcSQLSyntaxErrorException Таблица "USER_ACCOUNT_CREATED_EVENT" не найдена - PullRequest
1 голос
/ 23 апреля 2019

Резюме и первая проблема

Я пытаюсь проверить мой механизм регистрации пользователей. Когда новая учетная запись пользователя создается через мой REST API, в базе данных сохраняется UserAccountCreatedEvent. Запланированное задание проверяет базу данных каждые 5 секунд на наличие новых UserAccountCreatedEvent s и, если оно присутствует, отправляет электронное письмо зарегистрированному пользователю. При выполнении моих тестов я сталкиваюсь с проблемой, что таблица для UserAccountCreatedEvent не может быть найдена (см. Исключение ниже). Раньше я отправлял электронную почту способом блокировки в методе обслуживания, но недавно переключился на этот асинхронный подход. Все мои тесты отлично работали для подхода с блокировкой, и единственное, что я изменил для асинхронного подхода, - это включил в тест Awaitility.

2019-04-23 11:24:51.605 ERROR 7968 --- [taskScheduler-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task.

org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; SQL [select useraccoun0_.id as id1_0_, useraccoun0_.completed_at as complete2_0_, useraccoun0_.created_at as created_3_0_, useraccoun0_.in_process_since as in_proce4_0_, useraccoun0_.status as status5_0_, useraccoun0_.user_id as user_id1_35_ from user_account_created_event useraccoun0_ where useraccoun0_.status=? order by useraccoun0_.created_at asc limit ?]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException:
Table "USER_ACCOUNT_CREATED_EVENT" not found; SQL statement:
select useraccoun0_.id as id1_0_, useraccoun0_.completed_at as complete2_0_, useraccoun0_.created_at as created_3_0_, useraccoun0_.in_process_since as in_proce4_0_, useraccoun0_.status as status5_0_, useraccoun0_.user_id as user_id1_35_ from user_account_created_event useraccoun0_ where useraccoun0_.status=? order by useraccoun0_.created_at asc limit ? [42102-199]

Полная трассировка стека


Вторая проблема

Как будто этого было недостаточно, тесты ведут себя совершенно иначе при запуске их в режиме отладки. Когда я устанавливаю точку останова в методе, который вызывается методом, аннотированным @Scheduled, он вызывается несколько раз, хотя @Scheduled настроен с fixedDelayString (фиксированная задержка) 5000 мс. Благодаря регистрации я даже вижу, что было отправлено несколько писем. Тем не менее мой тестовый SMTP-сервер (GreenMail) не получает никаких писем. Как это вообще возможно? Я намеренно установил изоляцию транзакции на Isolation.SERIALIZABLE, чтобы было невозможно (насколько я понимаю изоляция транзакции), чтобы два запланированных метода получили доступ к одному и тому же событию из базы данных.


Третья проблема

В довершение всего, когда я перезапущу неудачные тесты, ОНИ РАБОТАЮТ. Но есть и другие исключения на консоли (см. Ниже). Но, тем не менее, приложение запускается и тесты завершаются успешно Существуют разные результаты тестирования в зависимости от того, запускаю ли я все тесты, только класс, только метод, только метод против повторных неудачных тестов. Я не понимаю, как такое неопределенное поведение может быть возможным.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: Failed to scan classpath for unlisted entity classes

Caused by: java.nio.channels.ClosedByInterruptException: null

Полная трассировка стека

Five tests fail Rerun and they work


Мой код

Тестовый класс (UserRegistrationTest)

@ActiveProfiles("test")
@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class UserRegistrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private Routes routes;

    @Autowired
    private TestConfig testConfig;

    @Resource(name = "validCustomerDTO")
    private CustomerDTO validCustomerDTO;

    @Resource(name = "validVendorDTO")
    private VendorRegistrationDTO validVendorRegistrationDTO;

    @Value("${schedule.sendRegistrationConfirmationEmailTaskDelay}")
    private Short registrationConfirmationEmailSenderTaskDelay;

    private GreenMail smtpServer;

    // Setup & tear down

    @Before
    public void setUp() {
        smtpServer = testConfig.getMailServer();
        smtpServer.start();
    }

    @After
    public void tearDown() {
        smtpServer.stop();
    }

    // Tests

    @Test
    public void testCreateCustomerAccount() throws Exception {
        mockMvc.perform(
            post(routes.getCustomerPath())
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(validCustomerDTO)))
            .andExpect(status().isCreated());

        // When run normally, I get a timeout from the next line
        await().atMost(registrationConfirmationEmailSenderTaskDelay + 10000, MILLISECONDS).until(smtpServerReceivedOneEmail());

        // Verify correct registration confirmation email was sent
        MimeMessage[] receivedMessages = smtpServer.getReceivedMessages();
        assertThat(receivedMessages).hasSize(1);

        // other checks
        // ...
    }

    @Test
    public void testCreateVendorAccount() throws Exception {
        mockMvc.perform(
            post(routes.getVendorPath())
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(validVendorRegistrationDTO)))
            .andExpect(status().isCreated());

        // When run normally, I get a timeout from the next line
        await().atMost(registrationConfirmationEmailSenderTaskDelay + 10000, MILLISECONDS).until(smtpServerReceivedOneEmail());

        // Verify correct registration confirmation email was sent
        MimeMessage[] receivedMessages = smtpServer.getReceivedMessages();
        assertThat(receivedMessages).hasSize(1);

        // other checks
        // ...
    }

    // Helper methods

    private Callable<Boolean> smtpServerReceivedOneEmail() {
        return () -> smtpServer.getReceivedMessages().length == 1;
    }

    // Test configuration

    @TestConfiguration
    static class TestConfig {

        private static final int PORT = 3025;
        private static final String HOST = "localhost";
        private static final String PROTOCOL = "smtp";

        GreenMail getMailServer() {
            return new GreenMail(new ServerSetup(PORT, HOST, PROTOCOL));
        }

        @Bean
        public JavaMailSender javaMailSender() {
            JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
            javaMailSender.setHost(HOST);
            javaMailSender.setPort(PORT);
            javaMailSender.setProtocol(PROTOCOL);
            javaMailSender.setDefaultEncoding("UTF-8");
            return javaMailSender;
        }
    }

Планировщик заданий (BusinessTaskScheduler)

@Component
public class BusinessTaskScheduler {

    private final RegistrationTask registrationTask;

    @Autowired
    public BusinessTaskScheduler(RegistrationTask registrationTask) {
        this.registrationTask = registrationTask;
    }

    @Scheduled(fixedDelayString = "${schedule.sendRegistrationConfirmationEmailTaskDelay}")
    public void sendRegistrationConfirmationEmail() {
        registrationTask.sendRegistrationConfirmationEmail();
    }
}

Код, который вызывается запланированным методом (RegistrationTask)

@Component
@Transactional(isolation = Isolation.SERIALIZABLE)
public class RegistrationTask {

    private final EmailHelper emailHelper;
    private final EventService eventService;
    private final UserRegistrationService userRegistrationService;

    @Autowired
    public RegistrationTask(EmailHelper emailHelper, EventService eventService, UserRegistrationService userRegistrationService) {
        this.emailHelper = emailHelper;
        this.eventService = eventService;
        this.userRegistrationService = userRegistrationService;
    }

    public void sendRegistrationConfirmationEmail() {
        Optional<UserAccountCreatedEvent> optionalEvent = eventService.getOldestUncompletedUserAccountCreatedEvent();
        if (optionalEvent.isPresent()) {
            UserAccountCreatedEvent event = optionalEvent.get();
            User user = event.getUser();
            RegistrationVerificationToken token = userRegistrationService.createRegistrationVerificationTokenForUser(user);
            emailHelper.sendRegistrationConfirmationEmail(token);
            eventService.completeEvent(event);
        }
    }
}

Служба событий (EventServiceImpl)

@Service
@Transactional(isolation = Isolation.SERIALIZABLE)
public class EventServiceImpl implements EventService {

    private final ApplicationEventDAO applicationEventDAO;
    private final UserAccountCreatedEventDAO userAccountCreatedEventDAO;

    @Autowired
    public EventServiceImpl(ApplicationEventDAO applicationEventDAO, UserAccountCreatedEventDAO userAccountCreatedEventDAO) {
        this.applicationEventDAO = applicationEventDAO;
        this.userAccountCreatedEventDAO = userAccountCreatedEventDAO;
    }

    @Override
    public void completeEvent(ApplicationEvent event) {
        if (!event.getStatus().equals(COMPLETED) && Objects.isNull(event.getCompletedAt())) {
            event.setStatus(COMPLETED);
            event.setCompletedAt(LocalDateTime.now());
            applicationEventDAO.save(event);
        }
    }

    @Override
    public Optional<UserAccountCreatedEvent> getOldestUncompletedUserAccountCreatedEvent() {
        Optional<UserAccountCreatedEvent> optionalEvent = userAccountCreatedEventDAO.findFirstByStatusOrderByCreatedAtAsc(NEW);
        if (optionalEvent.isPresent()) {
            UserAccountCreatedEvent event = optionalEvent.get();
            setEventInProcess(event);
            return Optional.of(userAccountCreatedEventDAO.save(event));
        }
        return Optional.empty();
    }

    @Override
    public void publishEvent(ApplicationEvent event) {
        applicationEventDAO.save(event);
    }

    // Helper methods

    private void setEventInProcess(ApplicationEvent event) {
        event.setStatus(Status.IN_PROCESS);
        event.setInProcessSince(LocalDateTime.now());
    }
}

UserAccountCreatedEvent

UML

application.yml

schedule:
  sendRegistrationConfirmationEmailTaskDelay: 5000 # delay between tasks in milliseconds

Я новичок в планировании работы со Spring, поэтому любая помощь очень ценится!

...