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
-
댓글
댓글 쓰기