Тестване на пролетна партидна работа

1. Въведение

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

В този урок ще разгледаме различните алтернативи за тестване на Spring Batch работа.

2. Необходими зависимости

Използваме spring-boot-starter-batch , така че първо нека зададем необходимите зависимости в нашия pom.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 

Включихме spring-boo t-starter-test и spring-batch-test, които включват някои необходими помощни методи, слушатели и бегачи за тестване на Spring Batch приложения.

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

Нека създадем просто приложение, което да покаже как Spring Batch решава някои от предизвикателствата за тестване.

Нашата молба използва два етапа на работа , която чете CSV входен файл с структурирани информационни книга и изходи книги и подробностите за книгите.

3.1. Определяне на стъпките за работа

Двете последваща стъпка и екстракт от конкретна информация от BookRecord а и след това картата те да Book ите (стъпка 1) и BookDetail ите (Стъпка 2):

@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. Дефиниране на четец на вход и писател на изход

Нека сега конфигурираме четеца за въвеждане на CSV файл с помощта на FlatFileItemReader, за да десериализираме структурираната информация за книгата в обекти на BookRecord :

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

В това определение има няколко важни неща, които ще имат последици за начина, по който тестваме.

На първо място, ние обозначили FlatItemReader боб с @StepScope , и като резултат, този обект ще споделя живота си с StepExecution .

Това също ни позволява да инжектираме динамични стойности по време на изпълнение, за да можем да предадем нашия входен файл от JobParameter s в ред 4 . За разлика от тях, маркерите, използвани за BookRecordFieldSetMapper, са конфигурирани по време на компилация.

След това дефинираме по подобен начин JsonFileItemWriter писател на изходи:

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

За втората стъпка използваме предоставен от Spring Batch ListItemWriter, който просто изхвърля неща в списък в паметта.

3.3. Дефиниране на персонализирания JobLauncher

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

Конфигурираме собствения си JobLauncher да предава потребителски екземпляр JobParameters при стартиране на Job :

@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 партида по време на тестването.

Нека създадем основна структура за нашия тест:

@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 s в нашите тестове.

Нашето приложение използва автоматично конфигуриране 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 да използват динамични стойности по време на изпълнение , което означава, че можем да предадем нашите 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.