测试因@Scheduled任务而失败:JdbcSQLSyntaxErrorException找不到表“ USER_ACCOUNT_CREATED_EVENT”

时间:2019-04-23 10:38:33

标签: spring hibernate spring-boot spring-data-jpa spring-test

摘要和第一个问题

我正在尝试测试我的用户注册机制。通过我的REST API创建新的用户帐户时,UserAccountCreatedEvent将存储在数据库中。计划任务每​​5秒检查数据库中是否有新的UserAccountCreatedEvent,如果存在,则向注册用户发送电子邮件。运行测试时,遇到找不到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]

Full stack trace


第二个问题

如果这还不够,那么在调试模式下运行测试时,它们的行为将完全不同。当我在用@Scheduled注释的方法所调用的方法中设置断点时,它多次被调用,而@Scheduled被配置为5000ms的fixedDelayString(固定延迟) 。多亏了日志记录,我什至可以看到已经发送了几封邮件。尽管如此,我的测试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

Full stack trace

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计划,因此非常感谢您的帮助!

0 个答案:

没有答案