96

When I run my unit tests, it invokes my scheduled tasks. I want to prevent this behaviour, which is caused by the fact that I have @EnableScheduling on my main app configuration.

How can I disable this on my unit tests?

I have come across this question/answer which suggests setting up profiles?

Not sure how I would go about that? or if its an overkill? I was thinking of having a separate AppConfiguration for my unit tests but it feels like im repeating code twice when I do that?

@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

}

Unit test example.

@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);
    }
}
Community
  • 1
  • 1
Robbo_UK
  • 11,351
  • 25
  • 81
  • 117

13 Answers13

108

If you don't want to use profiles, you can add flag that will enable/disable scheduling for the application

In your AppConfiguration add this

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

and in your test just add this annotation to disable scheduling

@TestPropertySource(properties = "app.scheduling.enable=false")
Marko Vranjkovic
  • 6,481
  • 7
  • 49
  • 68
  • 2
    somehow it killed my spring boot configurations – Hinotori May 24 '17 at 13:49
  • 8
    Be aware that external components could be enabling scheduling automatically (see HystrixStreamAutoConfiguration and MetricExportAutoConfiguration from the Spring Framework). So if you try and use `@ConditionalOnProperty` or `@Profile` on the `@Configuration` class that specifies `@EnableScheduling`, then scheduling will be enabled anyway due to external components. see https://stackoverflow.com/a/46783392/2947239 – Sisyphus Nov 03 '17 at 06:26
  • 1
    Sisyphus makes the same point I was going to make. I would add that MetricExportAutoConfiguration comes from Spring Boot Actuator and cane be disabled in a test profile with : spring.metrics.export.enabled: false – KC Baltz Feb 16 '18 at 21:08
  • 5
    As addition, you can set "app.scheduling.enable=false" in your application properties for tests, so that you don't need to add TestPropertySource annotation to each and every test. – nickolay.laptev Mar 30 '19 at 12:42
  • 2
    If it kills whole application, annotate particular beans containing `@Scheduled` with `@ConditionalOnProperty(value = "app.scheduling.enable", ... )` instead of of bean having `@EnableScheduling` annotation. – Michal Foksa Sep 17 '20 at 06:34
  • Alternatively, you can just move the `SchedulingConfiguration` out into its own class rather than be a static inner class, and drop the `@ConditionalOnProperty`. This will get picked up automatically if you use `@SpringBootApplication` but will get ignored by the tests. Note, you might have to tweak your scan paths to find the configurations. https://docs.spring.io/spring-boot/docs/2.4.13/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-detecting-config – Nigel Feb 08 '22 at 00:54
36

I just parameterized my @Scheduled annotation with configurable delay times:

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

In my test application.yaml:

timing:
    updateData: 60000
    initialDelay: 10000000000

And main application.yaml:

timing:
    updateData: 60000
    initialDelay: 1

It's not turning it off but creating such a long delay, the tests will be long over before it runs. Not the most elegant solution but definitely one of the easiest I've found.

Laila Sharshar
  • 389
  • 3
  • 4
  • 2
    Not a clean solution. but it does its work. Have an up-vote just for this different angle – Raja Anbazhagan Jul 31 '18 at 09:27
  • Out of the box thinking and also taught me a configuration I can use throughout my sever for setting a general fixed delay... Fantastic if you ask me :) – KeaganFouche Jun 06 '19 at 21:03
28

One more solution without any change in production code, using the @MockBean.

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

Which will eventually replace active scheduled job or create a mocked one.

From the documentation

Mocks can be registered by type or by {@link #name() bean name}. Any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.

Shafiul
  • 1,452
  • 14
  • 21
  • 4
    Very nice, best answer – smudo78 Jul 23 '20 at 10:37
  • 1
    But the question is how to disable the scheduling mechanism alone, as per your answer, the whole bean will be mocked, we cannot even use it as a normal spring bean. For me, @Laila Sharshar answer worked correctly – Prasanth Rajendran Sep 02 '20 at 19:33
  • 3
    This is by far the best and IMHO most clean answer. I always split out scheduling into a thin separate layer (i.e., an extra bean) to separate concerns, anyway, so @PrasanthRajendran comment doesn't apply to me. – Stefan Haberl Nov 30 '20 at 15:33
  • Keeping the scheduling in a separate bean is indeed a very good practice, otherwise single responsibility principle is not maintained properly. Thanks for explicitly mentioning that @StefanHaberl – Shafiul Dec 01 '20 at 23:04
21

An alternative would be to unregister the bean post processor that schedules the events. This can be done by simply putting the following class on the classpath of your tests:

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

While this is quite simple and seems to do the job, beware that I did not test this very much or check for possible implications of removing a defined bean from the registry or making sure that ordering of PostProcessors won't be an issue...

yankee
  • 38,872
  • 15
  • 103
  • 162
  • 2
    Just to give some feedback - I just tried this method and it appears to work just fine. – RockMeetHardplace Sep 06 '17 at 17:53
  • 2
    I have tried @vojtech-ruzicka and @lolotron answers plus other answers on [this similar question here](https://stackoverflow.com/questions/40226937/scheduled-method-is-called-during-the-tests) Likewise I must add that only @yankee answer worked for me. With a `@Configuration` annotation on his example `UnregisterScheduledProcessor ` class for it to be auto loaded by Spring under my test. So far I have not experience any issues with it. – DaddyMoe Oct 20 '17 at 15:35
  • 3
    This was the best solution for me, as I preferred not to modify existing production code just for the sake of tests. I simply added this to my test classpath as a Spring @Component, and it's working so far, great solution! – Oliver Hernandez Oct 03 '18 at 12:42
  • To reference the BeanFactory for the above solution: https://stackoverflow.com/questions/18487595/injecting-beanfactory-into-a-bean – Youness Mar 25 '20 at 18:47
17

With Spring Boot and cron expression you can enable or disable scheduling. For example you can define an test application.yml and set

scheduler:
  cron-expr: '-'

See also disable scheduling with '-'. In your scheduler class you can pass the expression.

@Scheduled(cron = "${scheduler.cron-expr}")
CodeSun
  • 206
  • 2
  • 12
5

Discovered that adding

app.scheduling.enable=false

in test application.properties along with

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

to scheduling configuration class annotations like in Marko Vranjkovic's answer works for all tests without need to annotate each of them!

Greeny grery
  • 51
  • 1
  • 1
2

In each Test you define which spring configuration should be used, currently you have:

@ContextConfiguration(classes={AppConfiguration.class})

Common practice is to define separate spring configuration for your normal application and for your tests.

AppConfiguration.java 
TestConfiguration.java

Then in your test you simply refference TestConfiguration instead of your current AppConfiguration using @ContextConfiguration(classes={TestConfiguration.class})

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

This way you can configure any setting for your tests differently than in production code. You can for example use in-memory database for your tests instead of regular one and much more.

Vojtech Ruzicka
  • 16,384
  • 15
  • 63
  • 66
  • 3
    Unfortunately it does not work that way. The test uses the TestConfiguration class but it still also uses the annotations declared on the TestConfiguration. :( – ali Jun 24 '16 at 16:18
  • You may set up a test configuration class or an active profiles to pick up from the properties file, however by doing this you don't avoid the annotated class being scheduled. – Kousick Shanmugam Nagaraj Dec 27 '16 at 09:35
  • 1
    http://stackoverflow.com/questions/40226937/scheduled-method-is-called-during-the-tests - This one worked for me – Kousick Shanmugam Nagaraj Dec 27 '16 at 10:52
2

I was able to solve this problem by creating a method that removes the scheduled tasks during unit tests. Here is an example:

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

    public static void removeScheduledTasks(ScheduledAnnotationBeanPostProcessor postProcessor, ApplicationContext appContext) {
        postProcessor.setApplicationContext(appContext);
        postProcessor.getScheduledTasks().forEach(ScheduledTask::cancel);   
    }
}

Use example:

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 scheduled tasks method
        Utils.removeScheduledTasks(postProcessor, appContext);
        
    }

    //Some test methods

}

Hope this helps.

crs
  • 26
  • 4
1

I was looking to do this in a normal class (not a unit test). I have my main Spring Boot application but needed a small utility class to do some bulk data cleanup. I wanted to use the full application context of my main app but turn off any scheduled tasks. The best solution for me was similar to Gladson Bruno:

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

Another advantage of this approach is you can get a list of all scheduled tasks, and you could add logic to cancel some tasks but not others.

Damon Horrell
  • 2,054
  • 1
  • 13
  • 9
1

create TestTaskScheduler Bean in test class

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;
        }
    }
}
pxzxj
  • 206
  • 2
  • 8
1

The way that I have solved this is for spring boot applications is by disabling the @EnableScheduling configuration for the test profile:

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 {
}
Dave Oh
  • 115
  • 1
  • 6
0

I would go for TestExecutionListener, example:

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

And then you add it to ur testClass

@TestExecutionListeners(listeners = SchedulerExecutionListener .class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
class Test {}
Ed .d
  • 23
  • 1
  • 3
0

My application properties are stored in application.yml so simple add ConditionalOnProperty to scheduler:

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

And disable develop environment in application.yml:

environments:
    development:
        scheduling:
            enabled: false
Maxmark
  • 1
  • 1