在春季测试中禁用@EnableSchedulingon

当我运行单元测试时,它会调用我预定的任务。我想防止这种行为,这是由于我有 @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);
}
}
50712 次浏览

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.

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...

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")

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.

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.

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.

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

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.

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!

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

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 {}