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.