19

How can I disable the schedule auto-start on Spring Boot IntegrationTest?

Thanks.

fassoline
  • 191
  • 1
  • 1
  • 6
  • Possible duplicate of [How to conditionally enable or disable scheduled jobs in Spring?](http://stackoverflow.com/questions/18406713/how-to-conditionally-enable-or-disable-scheduled-jobs-in-spring) – secondbreakfast Nov 18 '16 at 21:29
  • 1
    Possible duplicate of [Disable @EnableScheduling on Spring Tests](http://stackoverflow.com/questions/29014496/disable-enablescheduling-on-spring-tests) – ali Mar 08 '17 at 07:18
  • 1
    I can't believe there's no good answer to this question. Very surprising. – Ryan Shillington Mar 16 '17 at 16:47
  • The only solution I got to work was to use the @Profile - annotation, by creating a test profile and a production profile. ProTip: Do not forget to activate the production profile in your src/main/resources/application.properties if you want to avoid your colleagues hating you. ;) – ali Mar 17 '17 at 20:12

7 Answers7

22

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.

One solution

Have one @Configuration class that enables scheduling via @EnableScheduling, but then have your scheduled jobs in separate classes, each of those using @ConditionalOnProperty to enable/disable the classes that contain the @Scheduled tasks.

Don't have the @Scheduled and @EnableScheduling in the same class, or you will have the issue where external components are enabling it anyway, so the @ConditionalOnProperty is ignored.

Eg:

@Configuration
@EnableScheduling
public class MyApplicationSchedulingConfiguration {
}

and then in a separate class

@Named
@ConditionalOnProperty(value = "scheduling.enabled", havingValue = "true", matchIfMissing = false)
public class MyApplicationScheduledTasks {

  @Scheduled(fixedRate = 60 * 60 * 1000)
  public void runSomeTaskHourly() {
    doStuff();
  }
}

The issue with this solution is that every scheduled job needs to be in it's own class with @ConditionalOnProperty specified. If you miss that annotation, then the job will run.

Another Solution

Extend the ThreadPoolTaskScheduler and override the TaskScheduler methods. In these methods you can perform a check to see if the job should run.

Then, in your @Configuration class where you use @EnableScheduling, you also create a @Bean called taskScheduler which returns your custom thread pool task scheduler).

Eg:

public class ConditionalThreadPoolTaskScheduler extends ThreadPoolTaskScheduler {

  @Inject
  private Environment environment;

  // Override the TaskScheduler methods
  @Override
  public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    if (!canRun()) {
      return null;
    }
    return super.schedule(task, trigger);
  }

  @Override
  public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
    if (!canRun()) {
      return null;
    }
    return super.schedule(task, startTime);
  }

  @Override
  public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
    if (!canRun()) {
      return null;
    }
    return super.scheduleAtFixedRate(task, startTime, period);
  }

  @Override
  public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
    if (!canRun()) {
      return null;
    }
    return super.scheduleAtFixedRate(task, period);
  }

  @Override
  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
    if (!canRun()) {
      return null;
    }
    return super.scheduleWithFixedDelay(task, startTime, delay);
  }

  @Override
  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) {
    if (!canRun()) {
      return null;
    }
    return super.scheduleWithFixedDelay(task, delay);
  }

  private boolean canRun() {
    if (environment == null) {
      return false;
    }

    if (!Boolean.valueOf(environment.getProperty("scheduling.enabled"))) {
      return false;
    }

    return true;
  }
}

Configuration class that creates the taskScheduler bean using our custom scheduler, and enables scheduling

@Configuration
@EnableScheduling
public class MyApplicationSchedulingConfiguration {

  @Bean
  public TaskScheduler taskScheduler() {
    return new ConditionalThreadPoolTaskScheduler();
  }
}

The potential issue with the above is that you've created a dependency on an internal Spring class, so if there are changes in the future, you'd have to fix compatibility.

Duke0fAnkh
  • 301
  • 2
  • 5
10

I had the same problem. Tried Spring's @ConditionalOnProperty attribute with my Scheduling Bean but Scheduling still got activated in tests.

The only good workaround I found was to overwrite the scheduling properties in the Test class, so that the job does not have a real chance to run.

If your real job runs every 5 minutes using property my.cron=0 0/5 * * * *

public class MyJob {

    @Scheduled(cron = "${my.cron}")
    public void execute() {
        // do something
    }
} 

Then in the test class you can configure it as:

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {"my.cron=0 0 0 29 2 ?"}) // Configured as 29 Feb ;-)
public class MyApplicationTests {

    @Test
    public void contextLoads() {
    }

}

So even if your job is activated, it will only run at the 0th hour of 29 Feb which happens once in 4 years. So you have a very slim chance of running it.

You can come up with more fancy cron settings to suit your requirements.

MandeSin
  • 101
  • 1
  • 3
  • 2
    You could also set the cron value to `-` via properties, as this is reserved expression meaning never run – AndrewL Sep 22 '19 at 21:45
5

An easy solution I figured out in Spring Boot 2.0.3:

1) extract scheduled method(s) to a separate bean

@Service
public class SchedulerService {

  @Autowired
  private SomeTaskService someTaskService;

  @Scheduled(fixedRate = 60 * 60 * 1000)
  public void runSomeTaskHourly() {
    someTaskService.runTask();
  }
}

2) mock the scheduler bean in your test class

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

  @Autowired
  private SomeTaskService someTaskService;

  @MockBean
  private SchedulerService schedulerService;
}
Bunarro
  • 1,550
  • 1
  • 13
  • 8
2

One way is to use Spring profiles

In your test class:

@SpringBootTest(classes = Application.class)
@ActiveProfiles("integration-test")
public class SpringBootTestBase {
    ...
}

In your scheduler class or method:

@Configuration
@Profile("!integration-test") //to disable all from this configuration
public class SchedulerConfiguration {

    @Scheduled(cron = "${some.cron}")
    @Profile("!integration-test") //to disable a specific scheduler
    public void scheduler1() {
        // do something
    }

    @Scheduled(cron = "${some.cron}")
    public void scheduler2() {
        // do something
    }

    ...
}
Samuel Negri
  • 479
  • 4
  • 15
1

When your real Spring Boot Application class looks like this:

@SpringBootApplication   
@EnableScheduling
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

you would have to create another Application class without @EnableScheduling for your integration tests like this:

@SpringBootApplication   
public class MyTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyTestApplication.class, args);
    }

}

And then use the MyTestApplication class in your integration test like this

RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MyTestApplication.class)
public class MyIntegrationTest {

...
}

That's the way I do it, since I have not found a better way.

Tom
  • 3,913
  • 19
  • 28
  • 7
    This will only work in very special setups. Since `@SpringBootApplication` includes the `@ComponentScan` annotation, even if you specify MyTestApplication to be used by your tests, usually the `@ComponentScan` part is going to find the other class annotated with `@SpringBootApplication` apply its configuration as well and voila your `@EnableScheduling` is back on. For this to work you need to have both classes in different packages on the same level, which makes the whole auto configuration useless. – ali Mar 08 '17 at 07:10
  • Yes this looked so promising then as @ali suggest `@EnableScheduling` still kicked it – DaddyMoe Oct 20 '17 at 14:33
1

I solved this problem by using a separate configuration class and then overwrite this class in the test context. So instead of putting the annotation at the application I only put it at the separate configuration classes.
Normal context:

@Configuration
@EnableScheduling 
public class SpringConfiguration {}

Test context:

@Configuration
public class SpringConfiguration {}
Tobske
  • 518
  • 3
  • 12
0

Integrating some answers from above:

  • Create a separate configuration class for testing purposes (a.k.a "TestConfiguration.class")
  • Enable Mockito annotations there for other beans: schedulers, etc. - Read this: 2)

    @ConditionalOnClass
    @ConditionalOnMissingBean
    @ConditionalOnBean
    @ConditionalOnJava
    @ConditionalOnJndi
    @ConditionalOnMissingClass
    @ConditionalOnExpression
    @ConditionalOnNotWebApplication
    @ConditionalOnWebApplication
    @ConditionalOnProperty
    @ConditionalOnResource
    @ConditionalOnSingleCandidate
    

Plus, always checking:

  • "application.yml" self-creation properties depending on external devices/services
  • Auto-configuration annotations on Main class breaking beans initialization sequence
  • "applicationContext.xml", "beans.xml" or Classpath loaders

Please, read these:

Felix Aballi
  • 899
  • 1
  • 13
  • 31