在Spring测试中禁用@EnableScheduling

97
当我运行单元测试时,它会调用我的定时任务。我想要阻止这种行为,因为我的主应用程序配置中有 @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);
    }
}

你为什么想要在单元测试中使用Spring上下文? - Rohit Jain
那么它会自动装配我的对象,这样我就可以测试我的DAO和服务了吗? - Robbo_UK
你可以模拟 Dao。最好使用一些框架,比如 Mockito。 - Rohit Jain
1
正确答案不在此线程中,而是在这里找到:https://dev59.com/pVkS5IYBdhLWcg3wMj9I - Ryan Shillington
13个回答

109
如果您不想使用个人资料,您可以添加一个标志来启用/禁用应用程序的调度。在您的 AppConfiguration 中添加以下内容。
  @ConditionalOnProperty(
     value = "app.scheduling.enable", havingValue = "true", matchIfMissing = true
  )
  @Configuration
  @EnableScheduling
  public static class SchedulingConfiguration {
  }

在你的测试中,只需要添加这个注解来禁用调度

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

2
某种程度上它破坏了我的Spring Boot配置。 - Hinotori
9
请注意,外部组件可能会自动启用调度(请查看Spring框架中的HystrixStreamAutoConfiguration和MetricExportAutoConfiguration)。因此,如果您尝试在指定了@EnableScheduling的@Configuration类上使用@ConditionalOnProperty或@Profile,则由于外部组件的存在,调度仍将被启用。 请参见https://dev59.com/RFkR5IYBdhLWcg3w4hDN#46783392。 - Sisyphus
1
Sisyphus提出了我要表达的同样观点。我想补充一点,MetricExportAutoConfiguration来自Spring Boot Actuator,并且可以在测试配置文件中通过:spring.metrics.export.enabled: false禁用。 - KC Baltz
5
此外,您可以在应用程序属性中设置“app.scheduling.enable=false”以进行测试,这样您就不需要为每个测试添加TestPropertySource注释。 - nickolay.laptev
2
如果@Scheduled注释的特定bean会导致整个应用程序崩溃,请使用@ConditionalOnProperty(value = "app.scheduling.enable", ... )注释该bean,而不是使用@EnableScheduling注释的bean。 - Michal Foksa
或者,您可以将SchedulingConfiguration移动到自己的类中,而不是成为静态内部类,并删除@ConditionalOnProperty。如果您使用@SpringBootApplication,则会自动选择此选项,但测试将忽略它。请注意,您可能需要调整扫描路径以查找配置。 - Nigel

37

我刚刚使用可配置的延迟时间对我的@Scheduled注释进行了参数化:

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

在我的测试 application.yaml 文件中:

timing:
    updateData: 60000
    initialDelay: 10000000000

主要的application.yaml配置如下:

timing:
    updateData: 60000
    initialDelay: 1

它不是关闭它,而是创建一个如此漫长的延迟,以便在运行之前测试就能够完成。这不是最优雅的解决方案,但绝对是我发现的最简单的方案之一。


2
不是很优雅的解决方案,但它能完成工作。因为这种不同的角度,给你点赞。 - Raja Anbazhagan
独特的思维方式并且教会我一个配置方法,可以在整个服务器上设置通用的固定延迟... 如果你问我的话,这太棒了 :) - KeaganFouche

29

还有另一种解决方案,它不需要更改生产代码,使用 @MockBean

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

最终将替换活动计划任务或创建模拟任务。

来自文档的说明:

模拟任务可以按类型或{@link #name() bean名称}进行注册。如果上下文中已经存在相同类型的单个bean,则会被该模拟任务替换;如果没有定义现有的bean,则会添加一个新的bean。


4
非常好,最佳答案。 - smudo78
1
但问题是如何仅禁用调度机制,根据您的答案,整个bean将被模拟,我们甚至无法将其用作普通的Spring bean。对我来说,@Laila Sharshar的答案有效。 - Prasanth Rajendran
3
这是目前为止最好的答案,而且我认为它是最简洁明了的。无论如何,我总是将调度拆分成一个独立的层(即,额外的bean)来分离关注点,所以@PrasanthRajendran的评论并不适用于我。 - Stefan Haberl
将调度保持在单独的bean中确实是一个非常好的做法,否则单一职责原则就无法得到很好的维护。感谢@StefanHaberl明确提到这一点。 - Shafiul

21

另一种方法是注销安排事件的bean后处理器。您只需将以下类放在测试的类路径上即可完成此操作:

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或确保后处理器的顺序不会成为问题可能产生的影响...


2
只是提供一些反馈 - 我刚刚尝试了这种方法,看起来完全正常。 - RockMeetHardplace
2
我已经尝试了@vojtech-ruzicka和@lolotron的答案,以及此类似问题上的其他答案。同样地,我必须补充说,只有@yankee的答案对我有效。在他的示例UnregisterScheduledProcessor类上添加@Configuration注释,以便在我的测试中由Spring自动加载。到目前为止,我没有遇到任何问题。 - DaddyMoe
3
对我来说,这是最好的解决方案,因为我不想仅仅为了测试而修改现有的生产代码。我只需将其作为Spring @Component添加到我的测试类路径中,目前它运行良好,是个好的解决方案! - Oliver Hernandez
引用上述解决方案的BeanFactory:https://dev59.com/questions/A2Ml5IYBdhLWcg3wVlxf - Youness

17

使用Spring Boot和cron表达式,您可以启用或禁用调度。例如,您可以定义一个测试application.yml并设置

scheduler:
  cron-expr: '-'

还可以参阅使用“-”禁用调度。 在您的调度程序类中,您可以传递该表达式。

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

2
这应该成为新的被接受的答案;它比这里列出的所有其他内容都要好得多。 - InsaneOstrich

5

发现在测试的 application.properties 文件中加上以下内容:

app.scheduling.enable=false

即可实现禁用计划任务。

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

Marko Vranjkovic的答案中所示,使用类注释来进行调度配置,可以让所有测试都能够工作,而不需要为每个测试添加注释!


2

在每个测试中,您定义要使用哪个Spring配置,目前您有:

@ContextConfiguration(classes={AppConfiguration.class})

通常的做法是为正常应用程序和测试分别定义独立的Spring配置。

AppConfiguration.java 
TestConfiguration.java

在你的测试中,只需要使用@ContextConfiguration(classes={TestConfiguration.class})引用TestConfiguration而不是当前的AppConfiguration

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

这样可以为测试配置任何设置,与生产代码不同。 例如,您可以在测试中使用内存数据库而不是常规数据库等等。

3
很遗憾,它并不是那样工作的。该测试使用TestConfiguration类,但仍然使用在TestConfiguration上声明的注释。 :( - ali
您可以设置一个测试配置类或活动配置文件来从属性文件中选择,但这样做并不能避免注释类被调度。 - Kousick Shanmugam Nagaraj
1
这个对我有用。 - Kousick Shanmugam Nagaraj

2
我解决这个问题的方法是针对测试配置禁用@EnableScheduling注解,适用于Spring Boot应用程序。
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 {
}

2
我通过创建一个在单元测试期间删除预定任务的方法来解决了这个问题。以下是示例:
    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);   
    }
}

使用示例:

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

}

希望这有所帮助。

1

我想在普通类中实现这个功能(而不是单元测试)。我有一个主要的Spring Boot应用程序,但需要一个小型的实用程序类来进行批量数据清理。我想使用我的主应用程序的完整应用程序上下文,但关闭任何定时任务。对我来说最好的解决方案类似于Gladson Bruno:

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

这种方法的另一个优点是您可以获取所有已安排任务的列表,并且您可以添加逻辑来取消某些任务而不是其他任务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接