Отключить @EnableScheduling в весенних тестах

Когда я запускаю свои модульные тесты, он вызывает мои запланированные задачи. Я хочу предотвратить это поведение, которое вызвано тем, что у меня есть @EnableScheduling на моей основной конфигурации приложения.

Как я могу отключить это на моих модульных тестах?

Я сталкивался с этим вопросом / ответом, который предлагает настроить профили?

Не уверен, как бы я поступил об этом? или если это перебор? Я думал о создании отдельной AppConfiguration для моих модульных тестов, но мне кажется, что я повторяю код дважды, когда я делаю это?

@Configuration
@EnableJpaRepositories(AppConfiguration.DAO_PACKAGE)
@EnableTransactionManagement
@EnableScheduling
@ComponentScan({AppConfiguration.SERVICE_PACKAGE,
                AppConfiguration.DAO_PACKAGE,
                AppConfiguration.CLIENT_PACKAGE,
                AppConfiguration.SCHEDULE_PACKAGE})
public class AppConfiguration {

    static final    String MAIN_PACKAGE             = "com.etc.app-name";
    static final    String DAO_PACKAGE              = "com.etc.app-name.dao";
    private static  final  String ENTITIES_PACKAGE  = "com.etc.app-name.entity";
    static final    String SERVICE_PACKAGE          = "com.etc.app-name.service";
    static final    String CLIENT_PACKAGE           = "com.etc.app-name.client";
    static final    String SCHEDULE_PACKAGE         = "com.etc.app-name.scheduling";


    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
       // stripped code for question readability
    }

    // more app config code below etc

}

Пример юнит-теста.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={AppConfiguration.class})
@Transactional
@TransactionConfiguration(defaultRollback = true)
@WebAppConfiguration
public class ExampleDaoTest {

    @Autowired
    ExampleDao exampleDao;

    @Test
    public void testExampleDao() {
        List<Example> items = exampleDao.findAll();
        Assert.assertTrue(items.size()>0);
    }
}

13 ответов

Решение

Если вы не хотите использовать профили, вы можете добавить флаг, который будет включать / отключать планирование для приложения

В вашем AppConfiguration Добавь это

  @ConditionalOnProperty(
     value = "app.scheduling.enable", havingValue = "true", matchIfMissing = true
  )
  @Configuration
  @EnableScheduling
  public static class SchedulingConfiguration {
  }

и в своем тесте просто добавьте эту аннотацию, чтобы отключить планирование

@TestPropertySource(properties = "app.scheduling.enable=false")

Я просто параметризовал мою аннотацию @Scheduled с настраиваемым временем задержки:

@Scheduled(fixedRateString = "${timing.updateData}", initialDelayString = "${timing.initialDelay}")

В моем тестовом приложении. Yaml:

timing:
    updateData: 60000
    initialDelay: 10000000000

И главное приложение.yaml:

timing:
    updateData: 60000
    initialDelay: 1

Это не отключение, а создание такой длительной задержки, тесты пройдут задолго до того, как она запустится. Не самое элегантное решение, но одно из самых простых, которые я нашел.

Еще одно решение без каких-либо изменений в производственном коде с использованием @MockBean.

@RunWith(SpringRunner.class)
@SpringBootTest
@MockBean(MyScheduledClass.class)
public class MyTest {

Что в конечном итоге заменит активное запланированное задание или создаст имитацию.

Из документации

Моки могут быть зарегистрированы по типу или по имени компонента {@link #name()}. Любой существующий одиночный компонент того же типа, который определен в контексте, будет заменен имитацией, если существующий компонент не определен, будет добавлен новый.

Альтернативой может быть отмена регистрации постпроцессора бина, который планирует события. Это можно сделать, просто поместив следующий класс в classpath ваших тестов:

public class UnregisterScheduledProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
        for (String beanName : beanFactory.getBeanNamesForType(ScheduledAnnotationBeanPostProcessor.class)) {
            ((DefaultListableBeanFactory)beanFactory).removeBeanDefinition(beanName);
        }
    }
}

Хотя это довольно просто и, похоже, делает эту работу, остерегайтесь того, что я не слишком много тестировал или проверял возможные последствия удаления определенного bean-компонента из реестра или уверенности в том, что упорядочение PostProcessors не будет проблемой...

С помощью Spring Boot и cron вы можете включить или отключить планирование. Например, вы можете определить тестовый application.yml и установить

scheduler:
  cron-expr: '-'

См. Также отключение расписания с помощью '-'. В вашем классе планировщика вы можете передать выражение.

@Scheduled(cron = "${scheduler.cron-expr}")

Обнаружил, что добавление

app.scheduling.enable=false

в тестовом application.properties вместе с

      @ConditionalOnProperty(value = "app.scheduling.enable", havingValue = "true", matchIfMissing = true)
@EnableScheduling

для планирования аннотаций классов конфигурации, как в ответе Марко Враньковича, работает для всех тестов без необходимости аннотировать каждый из них!

В каждом тесте вы определяете, какая конфигурация пружины должна использоваться, в настоящее время у вас есть:

@ContextConfiguration(classes={AppConfiguration.class})

Обычной практикой является определение отдельной конфигурации пружины для вашего обычного применения и для ваших испытаний.

AppConfiguration.java 
TestConfiguration.java

Тогда в вашем тесте вы просто ссылка TestConfiguration вместо вашего текущего AppConfiguration с помощью @ContextConfiguration(classes={TestConfiguration.class})

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={TestConfiguration.class})
@Transactional
@TransactionConfiguration(defaultRollback = true)
@WebAppConfiguration
public class ExampleDaoTest

Таким образом, вы можете настроить любой параметр для ваших тестов иначе, чем в рабочем коде. Например, вы можете использовать базу данных в памяти для своих тестов вместо обычной и многое другое.

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

    import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
    import org.springframework.context.ApplicationContext;

    public static void removeSchedulledTasks(ScheduledAnnotationBeanPostProcessor postProcessor, ApplicationContext appContext) {

    postProcessor.setApplicationContext(appContext);

    Iterator<ScheduledTask> iterator = postProcessor.getScheduledTasks().iterator();

    while(iterator.hasNext()) {

        ScheduledTask taskAtual = iterator.next();
        taskAtual.cancel();

    }

}

Пример использования:

import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.example.Utils;


@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRemoveScheduller {


    @Autowired
    private ScheduledAnnotationBeanPostProcessor postProcessor;

    @Autowired
    private ApplicationContext appContext;


    @Before
    public void init(){

        //Some init variables

        //Remove schedulled tasks method
        Utils.removeSchedulledTasks(postProcessor, appContext);

    }

    //Some test methods

}

Надеюсь это поможет.

Я решил это для весенних загрузочных приложений, отключив конфигурацию @EnableScheduling для тестового профиля:

      import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@Profile({"!test"})
@EnableScheduling
public class SchedulingConfiguration {
}

Я хотел сделать это в обычном классе (а не в модульном тесте). У меня есть основное приложение Spring Boot, но мне нужен небольшой служебный класс для массовой очистки данных. Я хотел использовать весь контекст моего основного приложения, но отключить все запланированные задачи. Лучшее решение для меня было похоже на Gladson Bruno:

      scheduledAnnotationBeanPostProcessor.getScheduledTasks().forEach(ScheduledTask::cancel);

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

Создайте TestTaskSchedulerБин в тестовом классе

      public class TestTaskScheduler implements TaskScheduler {
    
    private static final NullScheduledFuture NULL_SCHEDULED_FUTURE = new NullScheduledFuture();
    
    @Override
    public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
        return NULL_SCHEDULED_FUTURE;
    }

    @Override
    public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
        return NULL_SCHEDULED_FUTURE;
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
        return NULL_SCHEDULED_FUTURE;
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
        return NULL_SCHEDULED_FUTURE;
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
        return NULL_SCHEDULED_FUTURE;
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) {
        return NULL_SCHEDULED_FUTURE;
    }
    
    private static class NullScheduledFuture implements ScheduledFuture {
        
        @Override
        public long getDelay(TimeUnit unit) {
            return 0;
        }

        @Override
        public int compareTo(Delayed o) {
            return 0;
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return false;
        }

        @Override
        public boolean isCancelled() {
            return false;
        }

        @Override
        public boolean isDone() {
            return false;
        }

        @Override
        public Object get() throws InterruptedException, ExecutionException {
            return null;
        }

        @Override
        public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            return null;
        }
    }
}

Я бы выбрал TestExecutionListener, например:

      public class SchedulerExecutionListener implements TestExecutionListener {

    @Override
    public void beforeTestClass(@NonNull TestContext testContext) {
        try {
            ScheduledAnnotationBeanPostProcessor schedulerProcessor = testContext.getApplicationContext().getBean(ScheduledAnnotationBeanPostProcessor.class);
            schedulerProcessor.destroy();
    } catch (Exception ignored) {
            ignored.printStackTrace();
            System.out.println(ignored.getMessage());
        }
}

И затем вы добавляете его в свой testClass

      @TestExecutionListeners(listeners = SchedulerExecutionListener .class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
class Test {}

Свойства моего приложения хранятся так просто: добавьте ConditionalOnProperty в планировщик:

      @ConditionalOnProperty(value = "scheduling.enabled", havingValue = "true", matchIfMissing = true)
@EnableScheduling

И отключите среду разработки вapplication.yml:

      environments:
    development:
        scheduling:
            enabled: false
Другие вопросы по тегам