1. 소개

다른 Spring 기반 애플리케이션과 달리 배치 작업 테스트에는 작업이 실행되는 방식의 비동기 특성으로 인해 몇 가지 특정 문제가 있습니다.

이 예제에서는 Spring Batch 작업 을 테스트하기위한 다양한 대안을 탐색 할 것입니다.

2. 필수 의존성

우리가 사용하고있는 스프링 부팅 스타터 배치를 그래서 먼저 우리의에서 필요한 종속까지의 설정을하자, pom.xml 파일 :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
    <version>2.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.4.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.batch</groupId>
    <artifactId>spring-batch-test</artifactId>
    <version>4.3.0.RELEASE</version>
    <scope>test</scope>
</dependency>

우리는 포함 Spring - 부의 t-선발 테스트스프링 배치 테스트  스프링 배치 응용 프로그램을 테스트하기 위해 몇 가지 필요한 도우미 메서드, 청취자와 주자에 가져.

3. 스프링 배치 작업 정의

Spring Batch가 테스트 문제를 어떻게 해결하는지 보여주는 간단한 애플리케이션을 만들어 보겠습니다.

우리 애플리케이션은 구조화 된 도서 정보가 포함 된 CSV 입력 파일을 읽고 도서 및 도서 세부 정보를 출력 하는 2 단계 작업사용합니다 .

3.1. 작업 단계 정의

두 개의 후속 단계BookRecord 에서 특정 정보를 추출한 다음이를 Book s (단계 1) 및 BookDetails ( 단계 2)에 매핑합니다 .

@Bean
public Step step1(
  ItemReader<BookRecord> csvItemReader, ItemWriter<Book> jsonItemWriter) throws IOException {
    return stepBuilderFactory
      .get("step1")
      .<BookRecord, Book> chunk(3)
      .reader(csvItemReader)
      .processor(bookItemProcessor())
      .writer(jsonItemWriter)
      .build();
}

@Bean
public Step step2(
  ItemReader<BookRecord> csvItemReader, ItemWriter<BookDetails> listItemWriter) {
    return stepBuilderFactory
      .get("step2")
      .<BookRecord, BookDetails> chunk(3)
      .reader(csvItemReader)
      .processor(bookDetailsItemProcessor())
      .writer(listItemWriter)
      .build();
}

3.2. 입력 판독기 및 출력 기록기 정의

이제 FlatFileItemReader사용하여 CSV 파일 입력 판독기를 구성하여 구조화 된 책 정보를 BookRecord 객체 직렬화 해 보겠습니다 .

private static final String[] TOKENS = { 
  "bookname", "bookauthor", "bookformat", "isbn", "publishyear" };

@Bean
@StepScope
public FlatFileItemReader<BookRecord> csvItemReader(
  @Value("#{jobParameters['file.input']}") String input) {
    FlatFileItemReaderBuilder<BookRecord> builder = new FlatFileItemReaderBuilder<>();
    FieldSetMapper<BookRecord> bookRecordFieldSetMapper = new BookRecordFieldSetMapper();
    return builder
      .name("bookRecordItemReader")
      .resource(new FileSystemResource(input))
      .delimited()
      .names(TOKENS)
      .fieldSetMapper(bookRecordFieldSetMapper)
      .build();
}

이 정의에는 몇 가지 중요한 사항이 있으며 이는 우리가 테스트하는 방식에 영향을 미칩니다.

우선, 우리는 어노테이션 FlatItemReader의 와 Bean @StepScope을 , 그리고 그 결과로, 이 객체와 수명 공유  StepExecution을 .

이를 통해 런타임에 동적 값을 주입하여 4 행 JobParameter 에서 입력 파일을 전달할 수 있습니다 . 반대로 BookRecordFieldSetMapper에 사용되는 토큰 은 컴파일 타임에 구성됩니다.

그런 다음 유사하게 JsonFileItemWriter 출력 작성기를 정의합니다 .

@Bean
@StepScope
public JsonFileItemWriter<Book> jsonItemWriter(
  @Value("#{jobParameters['file.output']}") String output) throws IOException {
    JsonFileItemWriterBuilder<Book> builder = new JsonFileItemWriterBuilder<>();
    JacksonJsonObjectMarshaller<Book> marshaller = new JacksonJsonObjectMarshaller<>();
    return builder
      .name("bookItemWriter")
      .jsonObjectMarshaller(marshaller)
      .resource(new FileSystemResource(output))
      .build();
}

두 번째 단계 에서는 메모리 내 List에 물건을 덤프 하는 Spring Batch 제공 ListItemWriter 를 사용합니다.

3.3. 사용자 지정 JobLauncher 정의

다음으로 application.properties 에서 spring.batch.job.enabled = false설정하여 Spring Boot Batch 의 기본 Job 시작 구성을 비활성화하겠습니다 .

Job을 시작할 때 커스텀 JobParameters 인스턴스 를 전달 하도록 자체 JobLauncher구성 합니다 .

@SpringBootApplication
public class SpringBatchApplication implements CommandLineRunner {

    // autowired jobLauncher and transformBooksRecordsJob

    @Value("${file.input}")
    private String input;

    @Value("${file.output}")
    private String output;

    @Override
    public void run(String... args) throws Exception {
        JobParametersBuilder paramsBuilder = new JobParametersBuilder();
        paramsBuilder.addString("file.input", input);
        paramsBuilder.addString("file.output", output);
        jobLauncher.run(transformBooksRecordsJob, paramsBuilder.toJobParameters());
   }

   // other methods (main etc.)
}

4. 스프링 배치 작업 테스트

스프링 배치 테스트 의존성은 테스트 동안 스프링 배치 컨텍스트를 구성하는 데 사용할 수있는 유용한 도우미 방법과 청취자의 집합을 제공합니다.

테스트를위한 기본 구조를 만들어 보겠습니다.

@RunWith(SpringRunner.class)
@SpringBatchTest
@EnableAutoConfiguration
@ContextConfiguration(classes = { SpringBatchConfiguration.class })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, 
  DirtiesContextTestExecutionListener.class})
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public class SpringBatchIntegrationTest {

    // other test constants
 
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;
  
    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;
  
    @After
    public void cleanUp() {
        jobRepositoryTestUtils.removeJobExecutions();
    }

    private JobParameters defaultJobParameters() {
        JobParametersBuilder paramsBuilder = new JobParametersBuilder();
        paramsBuilder.addString("file.input", TEST_INPUT);
        paramsBuilder.addString("file.output", TEST_OUTPUT);
        return paramsBuilder.toJobParameters();
   }

@SpringBatchTest 어노테이션은 제공 JobLauncherTestUtilsJobRepositoryTestUtils  헬퍼 클래스를. 테스트에서 JobStep 을 트리거하는 데 사용합니다 .

우리의 애플리케이션은 기본 메모리 내 JobRepository 를 활성화하는 Spring Boot 자동 구성 을 사용합니다 . 결과적으로 동일한 클래스에서 여러 테스트를 실행하려면 각 테스트 실행 후 정리 단계가 필요합니다 .

마지막으로 여러 테스트 클래스에서 여러 테스트를 실행하려면 컨텍스트를 dirty 로 표시해야합니다 . 이는 동일한 데이터 소스를 사용하는 여러 JobRepository  인스턴스 의 충돌을 방지하기 위해 필요합니다 .

4.1. 엔드-투-엔드 작업 테스트

가장 먼저 테스트 할 것은 작은 데이터 세트 입력 이있는 완전한 엔드 투 엔드 작업 입니다.

그런 다음 결과를 예상되는 테스트 출력과 비교할 수 있습니다.

@Test
public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception {
    // given
    FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT);
    FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);

    // when
    JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
    JobInstance actualJobInstance = jobExecution.getJobInstance();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
  
    // then
    assertThat(actualJobInstance.getJobName(), is("transformBooksRecords"));
    assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
    AssertFile.assertFileEquals(expectedResult, actualResult);
}

Spring Batch Test는 AssertFile 클래스를 사용하여 출력을 확인 하는 데 유용한 파일 비교 방법을 제공합니다 .

4.2. 개별 단계 테스트

전체 작업을 종단 간 테스트하는 데 비용이 많이 들기 때문에 대신 개별 단계 를 테스트하는 것이 좋습니다.

@Test
public void givenReferenceOutput_whenStep1Executed_thenSuccess() throws Exception {
    // given
    FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT);
    FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);

    // when
    JobExecution jobExecution = jobLauncherTestUtils.launchStep(
      "step1", defaultJobParameters()); 
    Collection actualStepExecutions = jobExecution.getStepExecutions();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();

    // then
    assertThat(actualStepExecutions.size(), is(1));
    assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
    AssertFile.assertFileEquals(expectedResult, actualResult);
}

@Test
public void whenStep2Executed_thenSuccess() {
    // when
    JobExecution jobExecution = jobLauncherTestUtils.launchStep(
      "step2", defaultJobParameters());
    Collection actualStepExecutions = jobExecution.getStepExecutions();
    ExitStatus actualExitStatus = jobExecution.getExitStatus();

    // then
    assertThat(actualStepExecutions.size(), is(1));
    assertThat(actualExitStatus.getExitCode(), is("COMPLETED"));
    actualStepExecutions.forEach(stepExecution -> {
        assertThat(stepExecution.getWriteCount(), is(8));
    });
}

통지 우리가 사용 launchStep의 트리거 특정 단계에 방법을 .

기억 우리는 우리의 설계 ItemReader 와  ItemWriter를  런타임에 동적 값을 사용 하는 방법, 우리는 우리의 I / O 파라미터를 전달할 수 JobExecution (라인 9 및 23).

첫 번째 단계 테스트에서는 실제 출력과 예상 출력을 비교합니다.

반면에 두 번째 테스트에서는 예상되는 항목에 대한 StepExecution확인합니다 .

4.3. 단계 범위 구성 요소 테스트

이제 FlatFileItemReader를 테스트 해 보겠습니다 . @StepScope으로 노출 했으므로 Spring Batch의 전용 지원을 사용하고 싶습니다 .

// previously autowired itemReader

@Test
public void givenMockedStep_whenReaderCalled_thenSuccess() throws Exception {
    // given
    StepExecution stepExecution = MetaDataInstanceFactory
      .createStepExecution(defaultJobParameters());

    // when
    StepScopeTestUtils.doInStepScope(stepExecution, () -> {
        BookRecord bookRecord;
        itemReader.open(stepExecution.getExecutionContext());
        while ((bookRecord = itemReader.read()) != null) {

            // then
            assertThat(bookRecord.getBookName(), is("Foundation"));
            assertThat(bookRecord.getBookAuthor(), is("Asimov I."));
            assertThat(bookRecord.getBookISBN(), is("ISBN 12839"));
            assertThat(bookRecord.getBookFormat(), is("hardcover"));
            assertThat(bookRecord.getPublishingYear(), is("2018"));
        }
        itemReader.close();
        return null;
    });
}

MetadataInstanceFactory는 사용자 정의 생성 StepExecution 우리 스텝 범위 주입 할 필요가 ItemReader를.

이 때문에 doInTestScope 메서드 를 사용하여 판독기의 동작을 확인할 수 있습니다 .

다음으로 JsonFileItemWriter를 테스트 하고 출력을 확인 하겠습니다 .

@Test
public void givenMockedStep_whenWriterCalled_thenSuccess() throws Exception {
    // given
    FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT_ONE);
    FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);
    Book demoBook = new Book();
    demoBook.setAuthor("Grisham J.");
    demoBook.setName("The Firm");
    StepExecution stepExecution = MetaDataInstanceFactory
      .createStepExecution(defaultJobParameters());

    // when
    StepScopeTestUtils.doInStepScope(stepExecution, () -> {
        jsonItemWriter.open(stepExecution.getExecutionContext());
        jsonItemWriter.write(Arrays.asList(demoBook));
        jsonItemWriter.close();
        return null;
    });

    // then
    AssertFile.assertFileEquals(expectedResult, actualResult);
}

이전 테스트와 달리 이제 테스트 개체를 완전히 제어 할 수 있습니다 . 결과적으로 우리는 I / O 스트림을 열고 닫을 책임이 있습니다 .

5. 결론

이 예제에서는 Spring Batch 작업을 테스트하는 다양한 접근 방식을 살펴 보았습니다.

종단 간 테스트는 작업의 전체 실행을 확인합니다. 개별 단계를 테스트하면 복잡한 시나리오에서 도움이 될 수 있습니다.

마지막으로 단계 범위 구성 요소의 경우 spring-batch-test에서 제공하는 여러 도우미 메서드를 사용할 수 있습니다 . 그들은 Spring Batch 도메인 객체를 스터 빙하고 조롱하는 데 도움을 줄 것입니다.

평소처럼 GitHub 에서 전체 코드베이스 탐색 할 수 있습니다 .