Перейти к основному содержимому

Как тестировать задания для Spring Batch?

· 7 мин. чтения

1. Введение

В отличие от других приложений на основе Spring, тестирование пакетных заданий связано с некоторыми специфическими проблемами, в основном из-за асинхронного характера выполнения заданий.

В этом руководстве мы собираемся изучить различные альтернативы для тестирования задания Spring Batch .

2. Требуемые зависимости

Мы используем spring-boot-starter-batch , поэтому сначала давайте настроим необходимые зависимости в нашем pom.xml :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.1</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-boo t-starter-test и spring-batch-test , которые привносят некоторые необходимые вспомогательные методы, прослушиватели и исполнители для тестирования приложений Spring Batch.

3. Определение пакетного задания Spring

Давайте создадим простое приложение, чтобы показать, как Spring Batch решает некоторые задачи тестирования.

В нашем приложении используется двухэтапное задание , которое считывает входной CSV-файл со структурированной информацией о книгах и выводит книги и сведения о книгах.

3.1. Определение шагов работы

Два последующих шага извлекают конкретную информацию из BookRecord и затем сопоставляют ее с Book (шаг 1) и BookDetail (шаг 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. Определение устройств чтения ввода и записи вывода

Давайте теперь настроим средство чтения входных данных CSV-файла с помощью FlatFileItemReader для десериализации структурированной информации о книге в объекты 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();
}

В этом определении есть пара важных моментов, которые повлияют на то, как мы тестируем.

Прежде всего, мы аннотировали bean- компонент FlatItemReader с помощью @StepScope , и в результате этот объект будет делиться своим временем жизни с StepExecution .

Это также позволяет нам вводить динамические значения во время выполнения, чтобы мы могли передать наш входной файл из JobParameters в строке 4 . Напротив, маркеры, используемые для 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();
}

На втором этапе мы используем ListItemWriter , предоставленный Spring Batch , который просто выгружает данные в список в памяти.

3.3. Определение пользовательского JobLauncher

Затем давайте отключим конфигурацию запуска заданий Spring Boot Batch по умолчанию, установив spring.batch.job.enabled=false в нашем application.properties.

Мы настраиваем наш собственный JobLauncher для передачи пользовательского экземпляра JobParameters при запуске задания :

@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. Тестирование пакетного задания Spring

Зависимость spring-batch-test предоставляет набор полезных вспомогательных методов и слушателей, которые можно использовать для настройки контекста Spring Batch во время тестирования.

Давайте создадим базовую структуру для нашего теста:

@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 предоставляет вспомогательные классы JobLauncherTestUtils и JobRepositoryTestUtils . Мы используем их для запуска Job и Step в наших тестах.

Наше приложение использует автоконфигурацию Spring Boot , которая включает по умолчанию JobRepository в памяти . В результате выполнение нескольких тестов в одном классе требует этапа очистки после каждого запуска теста .

Наконец, если мы хотим запустить несколько тестов из нескольких тестовых классов, нам нужно пометить наш контекст как грязный . Это необходимо, чтобы избежать конфликтов нескольких экземпляров 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 для использования динамических значений во время выполнения , что означает, что мы можем передавать наши параметры ввода-вывода в JobExecution (строки 9 и 23).

Для первого шага теста мы сравниваем фактический результат с ожидаемым.

С другой стороны, во втором тесте мы проверяем StepExecution для ожидаемых письменных элементов .

4.3. Тестирование компонентов с пошаговой областью действия

Давайте теперь протестируем FlatFileItemReader . Напомним, что мы представили его как bean-компонент @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);
}

В отличие от предыдущих тестов, теперь мы полностью контролируем наши тестовые объекты . В результате мы отвечаем за открытие и закрытие потоков ввода-вывода .

5. Вывод

В этом руководстве мы рассмотрели различные подходы к тестированию задания Spring Batch.

Сквозное тестирование проверяет полное выполнение задания. Тестирование отдельных шагов может помочь в сложных сценариях.

Наконец, когда дело доходит до компонентов с пошаговой областью действия, мы можем использовать набор вспомогательных методов, предоставляемых spring-batch-test. Они помогут нам заглушить и имитировать объекты домена Spring Batch.

Как обычно, мы можем изучить всю кодовую базу на GitHub .