Testen van een Spring Batch Job

1. Inleiding

In tegenstelling tot andere op Spring gebaseerde applicaties, brengt het testen van batchtaken enkele specifieke uitdagingen met zich mee, voornamelijk vanwege de asynchrone aard van de manier waarop taken worden uitgevoerd.

In deze tutorial gaan we de verschillende alternatieven verkennen voor het testen van een Spring Batch-taak.

2. Vereiste afhankelijkheden

We gebruiken spring-boot-starter-batch, dus laten we eerst de vereiste afhankelijkheden instellen in onze 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 

We hebben de lente-boe-geroept-startertest en lente-batch-test die enkele noodzakelijke hulpmethoden, luisteraars en hardlopers inbrengen voor het testen van Spring Batch-toepassingen.

3. Definiëren van de Spring Batch Job

Laten we een eenvoudige applicatie maken om te laten zien hoe Spring Batch enkele van de testuitdagingen oplost.

Onze applicatie maakt gebruik van twee stappen Job dat een CSV-invoerbestand met gestructureerde boekinformatie leest en boeken en boekdetails uitvoert.

3.1. De taakstappen definiëren

De twee volgende Staps extraheren specifieke informatie uit BookRecords en wijs deze vervolgens toe aan Boeks (stap1) en BookDetails (stap2):

@Bean public Stap step1 (ItemReader csvItemReader, ItemWriter jsonItemWriter) gooit IOException {return stepBuilderFactory .get ("step1"). chunk (3) .reader (csvItemReader) .processor (bookItemProcessor ()) .writer (jsonItemWriter) .build (); } @Bean public Stap step2 (ItemReader csvItemReader, ItemWriter listItemWriter) {return stepBuilderFactory .get ("step2"). chunk (3) .reader (csvItemReader) .processor (bookDetailsItemProcessor ()) .writer (listItemWriter) .build (); }

3.2. De inputlezer en outputschrijver definiëren

Laten we nu configureer de CSV-bestandsinvoerlezer met behulp van een FlatFileItemReader om de gestructureerde boekinformatie te deserialiseren naar BookRecord voorwerpen:

private static final String [] TOKENS = {"bookname", "bookauthor", "bookformat", "isbn", "publishyear"}; @Bean @StepScope openbare FlatFileItemReader csvItemReader (@Value ("# {jobParameters ['file.input']}") String invoer) {FlatFileItemReaderBuilder builder = nieuwe FlatFileItemReaderBuilder (); FieldSetMapper bookRecordFieldSetMapper = nieuwe BookRecordFieldSetMapper (); return builder .name ("bookRecordItemReader") .resource (nieuwe FileSystemResource (invoer)) .delimited () .names (TOKENS) .fieldSetMapper (bookRecordFieldSetMapper) .build (); }

Er zijn een paar belangrijke dingen in deze definitie die gevolgen zullen hebben voor de manier waarop we testen.

Allereerst, we hebben de FlatItemReader boon met @RTLnieuws, en als een resultaat, dit object zal zijn leven delen met Stapuitvoering.

Dit stelt ons ook in staat om dynamische waarden tijdens runtime te injecteren, zodat we ons invoerbestand van het JobParameters in lijn 4. De tokens die worden gebruikt voor de BookRecordFieldSetMapper zijn geconfigureerd tijdens het compileren.

We definiëren dan op dezelfde manier de JsonFileItemWriter output schrijver:

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

Voor de tweede Stapgebruiken we een meegeleverde Spring Batch ListItemWriter die gewoon dingen naar een in-memory lijst dumpt.

3.3. Het definiëren van de Custom JobLauncher

Laten we vervolgens de standaard uitschakelen Job de configuratie van Spring Boot Batch starten door in te stellen spring.batch.job.enabled = false in onze application.properties.

We configureren onze eigen JobLauncher om een ​​gewoonte door te geven JobParameters bijvoorbeeld bij het starten van de Job:

@SpringBootApplication openbare klasse SpringBatchApplication implementeert CommandLineRunner {// autowired jobLauncher en transformBooksRecordsJob @Value ("$ {file.input}") privé String-invoer; @Value ("$ {file.output}") privé String-uitvoer; @Override public void run (String ... args) gooit uitzondering {JobParametersBuilder paramsBuilder = nieuwe JobParametersBuilder (); paramsBuilder.addString ("file.input", invoer); paramsBuilder.addString ("file.output", output); jobLauncher.run (transformBooksRecordsJob, paramsBuilder.toJobParameters ()); } // andere methoden (hoofd enz.)} 

4. Testen van de Spring Batch Job

De lente-batch-test afhankelijkheid biedt een reeks nuttige hulpmethoden en luisteraars die kunnen worden gebruikt om de Spring Batch-context tijdens het testen te configureren.

Laten we een basisstructuur maken voor onze test:

@RunWith (SpringRunner.class) @SpringBatchTest @EnableAutoConfiguration @ContextConfiguration (classes = {SpringBatchConfiguration.class}) @TestExecutionListeners ({DependencyInjectionTestExecutionListener.class, DirtiesContextTest-class-Class-Execution) andere testconstanten @Autowired privé JobLauncherTestUtils jobLauncherTestUtils; @Autowired privé JobRepositoryTestUtils jobRepositoryTestUtils; @After openbare leegte cleanUp () {jobRepositoryTestUtils.removeJobExecutions (); } private JobParameters defaultJobParameters () {JobParametersBuilder paramsBuilder = nieuwe JobParametersBuilder (); paramsBuilder.addString ("file.input", TEST_INPUT); paramsBuilder.addString ("file.output", TEST_OUTPUT); retourneer paramsBuilder.toJobParameters (); } 

De @BuienRadarNL annotatie biedt de JobLauncherTestUtils en JobRepositoryTestUtils helper klassen. We gebruiken ze om het Job en Staps in onze tests.

Onze applicatie maakt gebruik van Spring Boot auto-configuratie, die een standaard in-memory mogelijk maakt JobRepository. Als resultaat, het uitvoeren van meerdere tests in dezelfde klasse vereist een opschoningsstap na elke testrun.

Tenslotte, als we meerdere tests willen uitvoeren vanuit verschillende testklassen, moeten we onze context als vuil markeren. Dit is nodig om het botsen van meerdere te voorkomen JobRepository instanties die dezelfde gegevensbron gebruiken.

4.1. Het end-to-end testen Job

Het eerste dat we zullen testen, is een complete end-to-end Job met een kleine data-set input.

We kunnen de resultaten dan vergelijken met een verwachte testoutput:

@Test openbare leegte gegevenReferenceOutput_whenJobExecuted_thenSuccess () gooit uitzondering {// gegeven FileSystemResource verwachtResult = nieuw FileSystemResource (EXPECTED_OUTPUT); FileSystemResource actualResult = nieuwe FileSystemResource (TEST_OUTPUT); // wanneer JobExecution jobExecution = jobLauncherTestUtils.launchJob (defaultJobParameters ()); JobInstance actualJobInstance = jobExecution.getJobInstance (); ExitStatus actualJobExitStatus = jobExecution.getExitStatus (); // dan assertThat (actualJobInstance.getJobName (), is ("transformBooksRecords")); assertThat (actualJobExitStatus.getExitCode (), is ("VOLTOOID")); AssertFile.assertFileEquals (verwachtResultaat, actueelResultaat); }

Spring Batch Test biedt een nuttige bestandsvergelijkingsmethode voor het verifiëren van uitvoer met behulp van de AssertFile klasse.

4.2. Individuele stappen testen

Soms is het vrij duur om alles te testen Job end-to-end en daarom is het zinvol om het individu te testen Stappen in plaats daarvan:

@Test openbare ongeldige gegevenReferenceOutput_whenStep1Executed_thenSuccess () gooit uitzondering {// gegeven FileSystemResource verwachtResult = nieuw FileSystemResource (EXPECTED_OUTPUT); FileSystemResource actualResult = nieuwe FileSystemResource (TEST_OUTPUT); // wanneer JobExecution jobExecution = jobLauncherTestUtils.launchStep ("step1", defaultJobParameters ()); Verzameling actualStepExecutions = jobExecution.getStepExecutions (); ExitStatus actualJobExitStatus = jobExecution.getExitStatus (); // dan assertThat (actualStepExecutions.size (), is (1)); assertThat (actualJobExitStatus.getExitCode (), is ("VOLTOOID")); AssertFile.assertFileEquals (verwachtResultaat, actueelResultaat); } @Test public void whenStep2Executed_thenSuccess () {// when JobExecution jobExecution = jobLauncherTestUtils.launchStep ("step2", defaultJobParameters ()); Verzameling actualStepExecutions = jobExecution.getStepExecutions (); ExitStatus actualExitStatus = jobExecution.getExitStatus (); // dan assertThat (actualStepExecutions.size (), is (1)); assertThat (actualExitStatus.getExitCode (), is ("VOLTOOID")); actualStepExecutions.forEach (stepExecution -> {assertThat (stepExecution.getWriteCount (), is (8));}); }

Let erop dat wij gebruiken de launchStep methode om specifieke stappen te activeren.

Onthoud dat we hebben ook onze ItemReader en ItemWriter om dynamische waarden tijdens runtime te gebruiken, wat betekent we kunnen onze I / O-parameters doorgeven aan de Jobuitvoering(regels 9 en 23).

Voor het eerst Stap test, vergelijken we de werkelijke output met een verwachte output.

Aan de andere kant, in de tweede test verifiëren we de Stapuitvoering voor de verwachte schriftelijke items.

4.3. Step-scoped componenten testen

Laten we nu het FlatFileItemReader. Bedenk dat we het hebben blootgelegd als @RTLnieuws bean, dus we willen hiervoor de speciale ondersteuning van Spring Batch gebruiken:

// eerder automatisch bekabelde itemReader @Test openbare leegte gegevenMockedStep_whenReaderCalled_thenSuccess () gooit uitzondering {// gegeven StepExecution stepExecution = MetaDataInstanceFactory .createStepExecution (defaultJobParameters ()); // when StepScopeTestUtils.doInStepScope (stepExecution, () -> {BookRecord bookRecord; itemReader.open (stepExecution.getExecutionContext ()); while ((bookRecord = itemReader.read ())! = null) {// dan 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;}); }

De MetadataInstanceFactory creëert een aangepaste Stapuitvoering dat is nodig om onze Step-scoped te injecteren ItemReader.

Door dit, we kunnen het gedrag van de lezer controleren met behulp van de doInTestScope methode.

Laten we vervolgens het JsonFileItemWriter en controleer de output:

@Test openbare leegte gegevenMockedStep_whenWriterCalled_thenSuccess () gooit uitzondering {// gegeven FileSystemResource verwachtResult = nieuw FileSystemResource (EXPECTED_OUTPUT_ONE); FileSystemResource actualResult = nieuwe FileSystemResource (TEST_OUTPUT); Boek demoBook = nieuw boek (); demoBook.setAuthor ("Grisham J."); demoBook.setName ("Het bedrijf"); StepExecution stepExecution = MetaDataInstanceFactory .createStepExecution (defaultJobParameters ()); // when StepScopeTestUtils.doInStepScope (stepExecution, () -> {jsonItemWriter.open (stepExecution.getExecutionContext ()); jsonItemWriter.write (Arrays.asList (demoBook)); jsonItemWriter.close (); return;}); // dan AssertFile.assertFileEquals (verwachtResultaat, actualResultaat); } 

In tegenstelling tot de vorige tests, we hebben nu de volledige controle over onze testobjecten. Als resultaat, wij zijn verantwoordelijk voor het openen en sluiten van de I / O-streams.

5. Conclusie

In deze zelfstudie hebben we de verschillende benaderingen van het testen van een Spring Batch-taak onderzocht.

End-to-end-tests verifiëren de volledige uitvoering van de taak. Het testen van afzonderlijke stappen kan helpen bij complexe scenario's.

Ten slotte, als het gaat om Step-scoped-componenten, kunnen we een aantal helper-methoden gebruiken die worden geboden door lente-batch-test. Zij zullen ons helpen bij het stoten en bespotten van Spring Batch-domeinobjecten.

Zoals gewoonlijk kunnen we de volledige codebase verkennen op GitHub.