为什么在尝试对整个 Spring 批处理作业进行单元测试时出现此错误?没有可用的 'org.springframework.batch.core.Job' 类型的合格 bean

Why this error trying to unit test an entire Spring Batch Job? No qualifying bean of type 'org.springframework.batch.core.Job' available

我正在开发 Spring 批处理应用程序。直到现在我才能够对诸如服务方法之类的东西进行单元测试(就像在每个 Spring 启动应用程序中所做的那样)。

现在我正在尝试按照本教程进行操作,以便从我的单元测试中测试整个作业 class(基本上我想执行一个执行作业的测试方法):https://www.baeldung.com/spring-batch-testing-job

这是我的 JUnit 测试 class,在这种情况下工作正常,我可以使用 @SpringBootTest 注释正确地测试我的服务方法:

@SpringBootTest
@SpringBatchTest
class UpdateInfoBatchApplicationTests {
    
    @Autowired
    private NotaryService notaryService;
    
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;
  
    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;
  
    @After
    public void cleanUp() {
    jobRepositoryTestUtils.removeJobExecutions();
    }
    
    @Autowired
    @Qualifier("launcher")
    private JobLauncher jobLauncher;
    
    @Autowired
    @Qualifier("updateNotaryDistrictsJob")
    private Job updateNotaryDistrictsJob;
    
    @Autowired
    @Qualifier("updateNotaryListInfoJob")
    private Job updateNotaryListInfoJob;
    
    private JobParameters defaultJobParameters() {
    JobParametersBuilder paramsBuilder = new JobParametersBuilder();
    //paramsBuilder.addString("file.input", TEST_INPUT);
    //paramsBuilder.addString("file.output", TEST_OUTPUT);
    return paramsBuilder.toJobParameters();
   }
    
    @Test
    public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception {
    // when
    JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
    JobInstance actualJobInstance = jobExecution.getJobInstance();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
    
    Assert.assertEquals(actualJobInstance.getJobName(), "updateNotaryDistrictsJob");
      
    // then
    //assertThat(actualJobInstance.getJobName(), is("updateNotaryDistrictsJob"));
    //assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
    //AssertFile.assertFileEquals(expectedResult, actualResult);
    }

    @Test
    void contextLoads() {
        System.out.println("TEST - contextLoads()");
    }
    
    @Test
    void getNotaryList() throws Exception {
    List<Notary> notaryList = this.notaryService.getNotaryList();
    System.out.println("notaryList size: " + notaryList);
    Assert.assertEquals("Notary List must be 5069", 5069, notaryList.size());
    }
    
    @Test
    void getNotaryDetails() throws Exception {
        NotaryDetails notaryDetails = this.notaryService.getNotaryDetails("089cy5Ra9zE%253D");
        System.out.println("notaryDetails: " + notaryDetails);
        Assert.assertEquals("Notary ID must be 089cy5Ra9zE%253D", "089cy5Ra9zE%253D", notaryDetails.getIdNotary());
    }
    
    @Test
    void getNotaryDistrictsList() throws Exception {
        List<NotaryDistrict> notaryDistrictsList = this.notaryService.getNotaryDistrictsList();
        System.out.println("notaryDistrictsList: " + notaryDistrictsList);
        
        Assert.assertEquals("Notary districts list lenght must be 91", 91, notaryDistrictsList.size());
        
        //ArrayList<NotaryDistrict>  notaryDistrictsListArrayList = new ArrayList<NotaryDistrict>(notaryDistrictsList);
        notaryDistrictsList.remove(0);
        Assert.assertEquals("Notary districts list lenght must now be 90", 90, notaryDistrictsList.size());
    }
    
    @Test
    void getNotaryDistrictDetails() throws Exception {
        NotaryDistrictDetails notaryDistrictDetails =  this.notaryService.getNotaryDistrictDetails("CG7drXn9fvA%253D");
        System.out.println("notaryDistrictDetails: " + notaryDistrictDetails.toString());
        
        Assert.assertEquals("Distretto must be: SCIACCA", "SCIACCA", notaryDistrictDetails.getDistretto());
    }
}

正如您在前面的代码中所见,我首先注入了我定义的两个 Job 对象:

@Autowired
@Qualifier("launcher")
private JobLauncher jobLauncher;

@Autowired
@Qualifier("updateNotaryDistrictsJob")
private Job updateNotaryDistrictsJob;

这些 Job 被定义为配置我的 Spring 批处理作业和步骤的 class 中的 bean,基本上我有这两个 bean:

@Bean("updateNotaryDistrictsJob")
public Job updateNotaryDistrictsListInfoJob(){
    return jobs.get("updateNotaryDistrictsListInfoJob")
            .incrementer(new RunIdIncrementer())
            .start(readNotaryDistrictsListStep())
            .build();
}

@Bean("updateNotaryListInfoJob")
public Job updateNotaryListInfoJob(){
    return jobs.get("updateNotaryListInfoJob")
            .incrementer(new RunIdIncrementer())
            .start(readNotaryListStep())
            .build();
}

然后在之前的测试中class有这个测试方法应该测试之前updateNotaryDistrictsJob作业的整个流程:

@Test
public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception {
    // when
    JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
    JobInstance actualJobInstance = jobExecution.getJobInstance();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
    
    Assert.assertEquals(actualJobInstance.getJobName(), "updateNotaryDistrictsJob");
}

问题是,当我运行这个测试方法时,我在我的堆栈跟踪中得到这个异常:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jobLauncherTestUtils': Unsatisfied dependency expressed through method 'setJob' parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.batch.core.Job' available: expected single matching bean but found 2: updateNotaryDistrictsJob,updateNotaryListInfoJob
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:768) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:720) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1413) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean[=17=](AbstractBeanFactory.java:335) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.9.jar:5.3.9]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.9.jar:5.3.9]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-2.5.3.jar:2.5.3]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[spring-boot-2.5.3.jar:2.5.3]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) ~[spring-boot-2.5.3.jar:2.5.3]
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:123) ~[spring-boot-test-2.5.3.jar:2.5.3]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) ~[spring-test-5.3.9.jar:5.3.9]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) ~[spring-test-5.3.9.jar:5.3.9]
    ... 69 common frames omitted
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.batch.core.Job' available: expected single matching bean but found 2: updateNotaryDistrictsJob,updateNotaryListInfoJob
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1358) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300) ~[spring-beans-5.3.9.jar:5.3.9]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:760) ~[spring-beans-5.3.9.jar:5.3.9]
    ... 88 common frames omitted

好像不能识别我的两个Job bean中必须使用哪个。

为什么?怎么了?我错过了什么?我该如何解决这个问题?

@SpringBatchTest 提供的JobLauncherTestUtils 期望在测试上下文中只有一个Job 类型的bean。这也记录在注释的 java 文档中。

如果您使用 @SpringBootTest 和完整组件扫描,以便拾取多个作业 bean,@SpringBatchTest 无法开箱即用。

最简单的解决方案可能是删除 @SpringBatchTest 并使用 jobLauncher 启动作业。或者,您可以将测试拆分为多个测试 类 并分别使用仅包含单个作业 bean 的测试上下文。