JobLauncherTestUtils를 이용한 Spring Batch Test
Spring Batch Test - JobLauncherTestUtils
- 이번에 스프링 배치 테스트 코드를 작성하던 도중에 고민하던 부분과 해결해보려 알아본 내용을 정리해봤습니다
스프링 배치 Step Flow 테스트
-
Batch 작업을 진행하다 보면 어느 한 Step에서 특정 조건에 부합하는 경우 Job을 종료해야하는 상황이 있을 수 있습니다.
-
그런 상황의 예제로 아래와 같은 Job의 마지막 실행일자가 오늘과 같으면 Job을 종료시키는 소스가 있습니다.
-
checkAlreadyRunJob()
을 보면 Job을 멈추기 위해서 Step의 ExitStauts를 임의로 셋업하여 Job을 멈추는 부분이 있습니다.-
Configuring for Stop 방법은 아래 링크를 참고했습니다. (xml, java config)
-
https://docs.spring.io/spring-batch/4.0.x/reference/html/step.html#configuringForStop
-
-
문제는
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으로 교체되어 등록됩니다.
-
앞에서는 ApplicationContext에 등록된 Bean을 Mock처리한다라고 말했지만, 확실하게는 ApplicationContext에 기존의 Bean과 같은 타입과 이름의 Mock 객체를 주입하여 기존의 Bean을 덮어쓰는 거였습니다.
-
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() 동작 순서
-
setUp에서 설정한 Job에서 StepName을 가지고 Step을 찾습니다.
-
내부에서 임의로 TestJob이란 것을 만들어서 찾아낸 Step을 추가하여 실행시킵니다.
-
따라서, 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
-
댓글
댓글 쓰기