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 .