Testando um trabalho em lote de primavera

Testando um trabalho em lote de primavera

1. Introdução

Diferentemente de outros aplicativos baseados no Spring, o teste de tarefas em lote apresenta alguns desafios específicos, principalmente devido à natureza assíncrona de como as tarefas são executadas.

Neste tutorial, vamos explorar as várias alternativas para testar um trabalhoSpring Batch.

2. Dependências necessárias

We’re using spring-boot-starter-batch, então primeiro vamos configurar as dependências necessárias em nossopom.xml:


    org.springframework.boot
    spring-boot-starter-batch
    2.1.9.RELEASE


    org.springframework.boot
    spring-boot-starter-test
    2.1.9.RELEASE
    test


    org.springframework.batch
    spring-batch-test
    4.2.0.RELEASE
    test

We included the spring-boot-starter-test and spring-batch-test which traz alguns métodos auxiliares, ouvintes e executores necessários para testar aplicativos Spring Batch.

3. Definindo o trabalho em lote da primavera

Vamos criar um aplicativo simples para mostrar como Spring Batch resolve alguns dos desafios de teste.

Nosso aplicativo usa umJob de duas etapas que lê um arquivo de entrada CSV com informações estruturadas do livro e produz livros e detalhes do livro.

3.1. Definindo as etapas da tarefa

Os doisSteps subsequentes extraem informações específicas deBookRecords e, em seguida, mapeiam-nas paraBooks (etapa1) eBookDetails (etapa2):

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

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

3.2. Definindo o leitor de entrada e o gravador de saída

Vamos agoraconfigure the CSV file input reader using a FlatFileItemReader desserializar as informações estruturadas do livro em objetosBookRecord:

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

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

Há algumas coisas importantes nessa definição, que terão implicações na maneira como testamos.

Em primeiro lugar,we annotated the FlatItemReader bean with*@StepScope*, e, como resultado,this object will share its lifetime with *StepExecution*.

This also allows us to inject dynamic values at runtime so that we can pass our input file from the JobParameters in line 4. Em contraste, os tokens usados ​​paraBookRecordFieldSetMapper são configurados em tempo de compilação.

Em seguida, definimos de forma semelhante o escritor de saídaJsonFileItemWriter:

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

Para o segundoStep, usamos umListItemWriter fornecido pelo Spring Batch que apenas despeja coisas em uma lista na memória.

3.3. Definindo oJobLauncher personalizado

Em seguida, vamos desativar a configuração de inicialização padrãoJob do Spring Boot Batch, definindospring.batch.job.enabled=false em nossoapplication.properties.

Configuramos nosso próprioJobLauncher para passar uma instânciaJobParameters personalizada ao iniciar oJob:

@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. Testando o trabalho em lote da primavera

A dependênciaspring-batch-test fornece um conjunto de métodos auxiliares e ouvintes úteis que podem ser usados ​​para configurar o contexto do Spring Batch durante o teste.

Vamos criar uma estrutura básica para nosso teste:

@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();
   }

The @SpringBatchTest annotation provides the JobLauncherTestUtils and JobRepositoryTestUtils helper classes. Nós os usamos para dispararJobeSteps em nossos testes.

Nosso aplicativo usaSpring Boot auto-configuration, which enables a default in-memory JobRepository. Como resultado,running multiple tests in the same class requires a cleanup step after each test run.

Finalmente,if we want to run multiple tests from several test classes, we need to mark our context as dirty. Isso é necessário para evitar o conflito de váriasJobRepository instâncias usando a mesma fonte de dados.

4.1. TestandoJob de ponta a ponta

A primeira coisa que testaremos é umJob completo de ponta a ponta com uma pequena entrada de conjunto de dados.

Podemos então comparar os resultados com uma saída de teste esperada:

@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);
}

O Spring Batch Test fornecefile comparison method for verifying outputs using the AssertFile class útil.

4.2. Testando etapas individuais

Às vezes é muito caro testar oJob completo de ponta a ponta e, portanto, faz sentido testarSteps individual em vez disso:

@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));
    });
}

Observe quewe use the launchStep method to trigger specific steps.

Lembre-se dewe also designed our ItemReader and ItemWriter to use dynamic values at runtime, o que significawe can pass our I/O parameters to the JobExecution (linhas 9 e 23).

Para o primeiro teste deStep, comparamos a saída real com uma saída esperada.

Por outro lado,in the second test, we verify the StepExecution for the expected written items.

4.3. Testando componentes com escopo definido

Vamos agora testar oFlatFileItemReader.Recall that we exposed it as @StepScope bean, so we’ll want to use Spring Batch’s dedicated support for this:

// 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;
    });
}

OMetadataInstanceFactory cria umStepExecution personalizado que é necessário para injetar nossoItemReader. com escopo Step

Por causa disso,we can check the behavior of the reader with the help of the doInTestScope method.

A seguir, vamos testar oJsonFileItemWritere verificar sua saída:

@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);
}

Ao contrário dos testes anteriores,we are now in full control of our test objects. Como resultado,we’re responsible for opening and closing the I/O streams.

5. Conclusão

Neste tutorial, exploramos as várias abordagens de teste de um job do Spring Batch.

O teste de ponta a ponta verifica a execução completa do trabalho. Testar etapas individuais pode ajudar em cenários complexos.

Finalmente, quando se trata de componentes com escopo Step, podemos usar vários métodos auxiliares fornecidos porspring-batch-test.. Eles nos ajudarão a criar stub e simular objetos de domínio do Spring Batch.

Como de costume, podemos explorar a base de código completaover on GitHub.