JobLauncherTestUtils를 이용한 Spring Batch Test

Spring Batch Test - JobLauncherTestUtils

  • 이번에 스프링 배치 테스트 코드를 작성하던 도중에 고민하던 부분과 해결해보려 알아본 내용을 정리해봤습니다

스프링 배치 Step Flow 테스트

  • Batch 작업을 진행하다 보면 어느 한 Step에서 특정 조건에 부합하는 경우 Job을 종료해야하는 상황이 있을 수 있습니다.

  • 그런 상황의 예제로 아래와 같은 Job의 마지막 실행일자가 오늘과 같으면 Job을 종료시키는 소스가 있습니다.

  • checkAlreadyRunJob()을 보면 Job을 멈추기 위해서 Step의 ExitStauts를 임의로 셋업하여 Job을 멈추는 부분이 있습니다.

  • 문제는 if (LocalDate.now().isEqual(lastRunDate))의 경우에 테스트 코드를 어떻게 작성하는 것이 좋을까 고민이 되었습니다.

  • 즉, 특정 상황에서의 Step의 흐름을 어떻게 하면 테스트할 수 있을까 알아보았습니다.

    • Step Exit Status가 STOPPED로 세팅되었는지

    • Step Exit Status(STOPPED)에 따라 Job이 정상 종료하였는지

...     
@Bean("userPrintJob")
public Job generateMyJob() {  
    return jobBuilderFactory.get("userPrintJob")  
        .start(generateMyTasklet())  
            .on("STOPPED").end()  
            .on("COMPLETED").to(generateMyStep()).end()  
        .build();  
}  
​  
@Bean("alreadyRunJobCheckStep")  
public Step generateMyTasklet() {  
    return this.stepBuilderFactory.get("alreadyRunJobCheckStep")  
        .tasklet(checkAlreadyRunJob())  
        .build();  
}  
​  
@Bean("userPrintStep")  
public Step generateMyStep() {  
    return stepBuilderFactory.get("userPrintStep")  
        .chunk(10)  
        .reader(readUserList())  
        .writer(writerUser())  
        .build();  
}  
​  
@Bean("alreadyRunJobCheckTasklet")  
@StepScope  
public Tasklet checkAlreadyRunJob() {  
    return (StepContribution contribution, ChunkContext chunkContext) -> {  
        LocalDate lastRunDate = LocalDate.parse(userDao.selectJobLastRunDate());  
​  
        if (LocalDate.now().isEqual(lastRunDate)) {  
            StepExecution stepExec = StepSynchronizationManager.getContext().getStepExecution();  
            stepExec.setExitStatus(ExitStatus.STOPPED);  
        }  
​  
        return RepeatStatus.FINISHED;  
    };  
}

스프링 배치 테스트 유틸 클래스 - JobLauncherTestUtils

  • 스프링 배치 테스트를 도와주는 것이 뭐가 있을까 찾아보니 다음과 같은 유틸 클래스가 있었습니다.

  • 해당 클래스는 각각의 Step을 개별적으로 end to end 테스트할 수 있도록 도와주는 클래스입니다.

    end to end 테스트란?
    • test whether the flow of an application is performing as designed from start to finish.
    • 어떤 어플리케이션 흐름이 처음부터 끝까지 설계된 대로 수행되는지 테스트하는 것을 말합니다.
  • 즉, spring-batch-test에서 제공하는 JobLauncherTestUtils 클래스를 이용하면, Job이나 Step 단위를 end to end 테스트를 진행할 수 있습니다.

  • 한가지 단점이라고 생각하는 부분은 해당 클래스를 이용하여 테스트를 하기위해서는 ApplicationContext를 띄워서 Job과 Step을 실행하기 위한 Bean을 등록해야한다는 것입니다.

    • 단점이라고 생각한 부분은 테스트 클래스가 실행될 때마다 비교적 시간이 많이 소요되기 때문입니다.

    • 또한, ApplicationContext에 등록된 Bean은 @Mock을 이용해 낮은 의존성의 테스트를 진행 할 수 없었습니다.



ApplicationContext에 등록한 Bean, Mock 처리하기

  • 위의 DOC 문서를 따라 테스트를 진행하다보니, 실제로 ApplicationContext를 로드해서 Job과 Step을 실행하면 DB에 영향을 끼칠 수도 있고 테스트하고자하는 단위에 의존성이 생겼습니다.

  • 즉, 독립적인 단위 테스트를 위해 ApplicationContext에 등록된 Bean을 Mock 처리하여 테스트를 진행할 수 없었습니다.

  • 그런데 boot에서 ApplicationContext에 등록된 Bean을 Mock 처리할 수 있는 기능이 있었습니다.

    • boot가 아닌 경우에도 할 수 있는 방법이 있는지, 아시는 분이 계시다면 말씀해주시면 감사드리겠습니다.

@MockBean

  • spring-boot에서 제공되는 어노테이션으로, 해당 어노테이션을 사용하는 경우 ApplicationContext에 Mock 처리한 Bean을 추가하여 사용할 수 있습니다. 만약 기존에 같은 타입의 Bean이 있다면 MockBean으로 교체되어 등록됩니다.

RunWith(SpringRunner.class)  
@SpringBootTest(classes = {BootBatchApplication.class, TestUtilContext.class})  
public class TestAppApplicationTests {  
    @Autowired  
    private Job userPrintJob;  
​  
    @MockBean(name = "userReader")  
    private ItemReader<User> userReader;  
​  
    @MockBean(name = "userDao")  
    private UserDao userDao;  
​  
    @Autowired  
    private JobLauncherTestUtils jobLauncherTestUtils;  
​  
    @Before  
    public void setUp() {  
        jobLauncherTestUtils.setJob(userPrintJob);  
    }  
​  
    @Test  
    public void testAlreadyRunCheck_notAlreadyRun() {  
        String yesterday = LocalDate.now().minusDays(1).toString();  
​  
        when(userDao.selectJobLastRunDate()).thenReturn(yesterday);  
​  
        JobExecution jobExecution = jobLauncherTestUtils.launchStep("alreadyRunJobCheckStep");  
        StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();  
        assertEquals("COMPLETED", stepExecution.getExitStatus().getExitCode());  
    }  
​  
    @Test  
    public void testAlreadyRunCheck_alreadyRun() {  
        String today = LocalDate.now().toString();  
​  
        when(userDao.selectJobLastRunDate()).thenReturn(today);  
​  
        JobExecution jobExecution = jobLauncherTestUtils.launchStep("alreadyRunJobCheckStep");  
        StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();  
        assertEquals("STOPPED", stepExecution.getExitStatus().getExitCode());  
    }
@Configuration  
public class TestUtilContext {  
    @Bean  
    public JobLauncherTestUtils jobLauncherTestUtils() {  
        return new JobLauncherTestUtils();  
    }  
}


추가 설명

JobLauncherTestUtils 클래스

  • spring batch test 2.1 부터 추가된 클래스

  • laucherStep() 동작 순서

    1. setUp에서 설정한 Job에서 StepName을 가지고 Step을 찾습니다.

    2. 내부에서 임의로 TestJob이란 것을 만들어서 찾아낸 Step을 추가하여 실행시킵니다.

    3. 따라서, Batch Log 테이블에는 아래와 같이 저장됩니다.

    public JobExecution launchStep(String stepName) {  
        return this.launchStep(stepName, this.getUniqueJobParameters(), null);  
    }  
        
    public JobExecution launchStep(String stepName, JobParameters jobParameters, ExecutionContext jobExecutionContext) {  
        if (!(job instanceof StepLocator)) {  
            throw new UnsupportedOperationException(...);  
        }  
        
        StepLocator locator = (StepLocator) this.job;  
        Step step = locator.getStep(stepName);  
        
        if (step == null) {  
            step = locator.getStep(this.job.getName() + "." + stepName);  
        }  
        
        if (step == null) {  
            throw new IllegalStateException("No Step found with name: [" + stepName + "]");  
        }  
        
        return getStepRunner().launchStep(step, jobParameters, jobExecutionContext);  
    }
    
      
    public JobExecution launchStep(Step step, JobParameters jobParameters, final ExecutionContext jobExecutionContext) {  
        SimpleJob job = new SimpleJob();  
        job.setName("TestJob");  
        job.setJobRepository(this.jobRepository);  
    ​  
        List<Step> stepsToExecute = new ArrayList<Step>();  
        stepsToExecute.add(step);  
        job.setSteps(stepsToExecute);  
    ​  
        ...  
    ​  
        return this.launchJob(job, jobParameters);  
    }
    
JOB_INSTANCE_ID JOB_NAME JOB_KEY STATUS EXIT_CODE
176 TestJob f371cdaa8145… COMPLETED STOPPED
175 TestJob f4519ccf6e657… COMPLETED COMPLETED

@MockBean 어노테이션

  • spring boot test 1.4부터 추가된 어노테이션

  • 해당 어노테이션은 Runwith(SpringRunner.class)가 붙은 테스트 클래스에서 동작합니다.

    • SpringRunner.class는 junit 4.12 이상을 요구

    • spring boot test 1.4 디펜던시 보면 junit 버전이 4.12

댓글