diff --git a/2025-05/spring-26-spring-batch/.gitignore b/2025-05/spring-26-spring-batch/.gitignore
new file mode 100644
index 00000000..7622f151
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/.gitignore
@@ -0,0 +1,9 @@
+.idea/
+*.iml
+
+target/
+
+output.csv
+*.log
+output*.dat
+test-output.dat
diff --git a/2025-05/spring-26-spring-batch/entries.csv b/2025-05/spring-26-spring-batch/entries.csv
new file mode 100644
index 00000000..bde433ce
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/entries.csv
@@ -0,0 +1,16 @@
+Ivan,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+Mary,24
+Ivan,23
+John,24
+Sunny,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+John,24
diff --git a/2025-05/spring-26-spring-batch/pom.xml b/2025-05/spring-26-spring-batch/pom.xml
new file mode 100644
index 00000000..a6929164
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/pom.xml
@@ -0,0 +1,131 @@
+
+
+ 4.0.0
+
+ ru.otus.example
+ spring-batch-demo
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+
+ 3.5.3
+
+
+
+
+ 17
+ 17
+ 17
+ 4.3.8
+ 4.11.0
+ 2.2.220
+ 2.0
+ 32.1.2-jre
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-batch
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.yaml
+ snakeyaml
+ ${snakeyaml.version}
+
+
+
+ com.h2database
+ h2
+ runtime
+ ${h2.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+ ${flapdoodle.version}
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo.spring30x
+ ${flapdoodle.version}
+
+
+
+ com.github.cloudyrock.mongock
+ mongock-spring-v5
+ ${mongock.version}
+
+
+ com.google.guava
+ guava
+
+
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+ com.github.cloudyrock.mongock
+ mongodb-springdata-v3-driver
+ ${mongock.version}
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.shell
+ spring-shell-starter
+ 3.4.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.batch
+ spring-batch-test
+ test
+
+
+
+
+ spring-batch-demo
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java
new file mode 100644
index 00000000..58b6edf6
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/Main.java
@@ -0,0 +1,16 @@
+package ru.otus.example.springbatch;
+
+import com.github.cloudyrock.spring.v5.EnableMongock;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@EnableMongock
+@SpringBootApplication
+public class Main {
+ // --spring.shell.interactive.enabled=false --spring.batch.job.enabled=true inputFileName=entries.csv outputFileName=output_new.dat
+ public static void main(String[] args) {
+ SpringApplication.run(Main.class, args);
+ }
+}
+
+
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java
new file mode 100644
index 00000000..feb52399
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java
@@ -0,0 +1,45 @@
+package ru.otus.example.springbatch.chandgelogs;
+
+import com.github.cloudyrock.mongock.ChangeLog;
+import com.github.cloudyrock.mongock.ChangeSet;
+import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate;
+import com.mongodb.client.MongoDatabase;
+import ru.otus.example.springbatch.model.Person;
+
+@ChangeLog(order = "001")
+public class InitMongoDBDataChangeLog {
+
+ @ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true)
+ public void dropDB(MongoDatabase database){
+ database.drop();
+ }
+
+ @ChangeSet(order = "001", id = "initPersons", author = "stvort", runAlways = true)
+ public void initPersons(MongockTemplate template){
+ template.save(new Person("Джон", 21));
+ template.save(new Person("Игорь", 32));
+ template.save(new Person("Дмитрий", 52));
+ template.save(new Person("Михаил", 22));
+ template.save(new Person("Герман", 33));
+ template.save(new Person("Джон", 21));
+ template.save(new Person("Игорь", 32));
+ template.save(new Person("Дмитрий", 52));
+ template.save(new Person("Михаил", 22));
+ template.save(new Person("Герман", 33));
+ template.save(new Person("Джон", 21));
+ template.save(new Person("Игорь", 32));
+ template.save(new Person("Дмитрий", 52));
+ template.save(new Person("Михаил", 22));
+ template.save(new Person("Герман", 33));
+ template.save(new Person("Джон", 21));
+ template.save(new Person("Игорь", 32));
+ template.save(new Person("Дмитрий", 52));
+ template.save(new Person("Михаил", 22));
+ template.save(new Person("Герман", 33));
+ template.save(new Person("Джон", 21));
+ template.save(new Person("Игорь", 32));
+ template.save(new Person("Дмитрий", 52));
+ template.save(new Person("Михаил", 22));
+ template.save(new Person("Герман", 33));
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java
new file mode 100644
index 00000000..5d9deb61
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/AppProps.java
@@ -0,0 +1,14 @@
+package ru.otus.example.springbatch.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties("app")
+public class AppProps {
+ private String inputFile;
+ private String outputFile;
+
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java
new file mode 100644
index 00000000..276e92b9
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java
@@ -0,0 +1,17 @@
+package ru.otus.example.springbatch.config;
+
+import org.springframework.batch.core.configuration.JobRegistry;
+import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@SuppressWarnings("unused")
+//@Configuration
+public class BatchConfig {
+ @Bean
+ public JobRegistryBeanPostProcessor postProcessor(JobRegistry jobRegistry) {
+ var processor = new JobRegistryBeanPostProcessor();
+ processor.setJobRegistry(jobRegistry);
+ return processor;
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java
new file mode 100644
index 00000000..2f1a81ed
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/config/JobConfig.java
@@ -0,0 +1,188 @@
+package ru.otus.example.springbatch.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.*;
+import org.springframework.batch.core.configuration.annotation.StepScope;
+import org.springframework.batch.core.job.builder.JobBuilder;
+import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.step.builder.StepBuilder;
+import org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.file.FlatFileItemReader;
+import org.springframework.batch.item.file.FlatFileItemWriter;
+import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
+import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
+import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
+import org.springframework.batch.item.file.transform.DelimitedLineAggregator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.lang.NonNull;
+import org.springframework.transaction.PlatformTransactionManager;
+import ru.otus.example.springbatch.model.Person;
+import ru.otus.example.springbatch.service.CleanUpService;
+import ru.otus.example.springbatch.service.HappyBirthdayService;
+
+import java.util.List;
+
+
+@SuppressWarnings("unused")
+@Configuration
+public class JobConfig {
+ private static final int CHUNK_SIZE = 5;
+ private final Logger logger = LoggerFactory.getLogger("Batch");
+
+ public static final String OUTPUT_FILE_NAME = "outputFileName";
+ public static final String INPUT_FILE_NAME = "inputFileName";
+ public static final String IMPORT_USER_JOB_NAME = "importUserJob";
+
+ @Autowired
+ private JobRepository jobRepository;
+
+ @Autowired
+ private PlatformTransactionManager platformTransactionManager;
+
+
+ @Autowired
+ private CleanUpService cleanUpService;
+
+ @StepScope
+ @Bean
+ public FlatFileItemReader reader(@Value("#{jobParameters['" + INPUT_FILE_NAME + "']}") String inputFileName) {
+ return new FlatFileItemReaderBuilder()
+ .name("personItemReader")
+ .resource(new FileSystemResource(inputFileName))
+
+ .delimited()
+ .names("name", "age")
+ .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{
+ setTargetType(Person.class);
+ }}).build();
+ }
+
+ @StepScope
+ @Bean
+ public ItemProcessor processor(HappyBirthdayService happyBirthdayService) {
+ return happyBirthdayService::doHappyBirthday;
+ }
+
+ @StepScope
+ @Bean
+ public FlatFileItemWriter writer(@Value("#{jobParameters['" + OUTPUT_FILE_NAME + "']}") String outputFileName) {
+ return new FlatFileItemWriterBuilder()
+ .name("personItemWriter")
+ .resource(new FileSystemResource(outputFileName))
+ .lineAggregator(new DelimitedLineAggregator<>())
+ .build();
+ }
+
+
+ @Bean
+ public MethodInvokingTaskletAdapter cleanUpTasklet() {
+ MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
+
+ adapter.setTargetObject(cleanUpService);
+ adapter.setTargetMethod("cleanUp");
+
+ return adapter;
+ }
+
+
+ @Bean
+ public Job importUserJob(Step transformPersonsStep, Step cleanUpStep) {
+ return new JobBuilder(IMPORT_USER_JOB_NAME, jobRepository)
+ .incrementer(new RunIdIncrementer())
+ .flow(transformPersonsStep)
+ .next(cleanUpStep)
+ .end()
+ .listener(new JobExecutionListener() {
+ @Override
+ public void beforeJob(@NonNull JobExecution jobExecution) {
+ logger.info("Начало job");
+ }
+
+ @Override
+ public void afterJob(@NonNull JobExecution jobExecution) {
+ logger.info("Конец job");
+ }
+ })
+ .build();
+ }
+
+ @Bean
+ public Step transformPersonsStep(ItemReader reader, FlatFileItemWriter writer,
+ ItemProcessor itemProcessor) {
+ return new StepBuilder("transformPersonsStep", jobRepository)
+ .chunk(CHUNK_SIZE, platformTransactionManager)
+ .reader(reader)
+ .processor(itemProcessor)
+ .writer(writer)
+ .listener(new ItemReadListener<>() {
+ public void beforeRead() {
+ logger.info("Начало чтения");
+ }
+
+ public void afterRead(@NonNull Person o) {
+ logger.info("Конец чтения");
+ }
+
+ public void onReadError(@NonNull Exception e) {
+ logger.info("Ошибка чтения");
+ }
+ })
+ .listener(new ItemWriteListener() {
+ public void beforeWrite(@NonNull List list) {
+ logger.info("Начало записи");
+ }
+
+ public void afterWrite(@NonNull List list) {
+ logger.info("Конец записи");
+ }
+
+ public void onWriteError(@NonNull Exception e, @NonNull List list) {
+ logger.info("Ошибка записи");
+ }
+ })
+ .listener(new ItemProcessListener<>() {
+ public void beforeProcess(@NonNull Person o) {
+ logger.info("Начало обработки");
+ }
+
+ public void afterProcess(@NonNull Person o, Person o2) {
+ logger.info("Конец обработки");
+ }
+
+ public void onProcessError(@NonNull Person o, @NonNull Exception e) {
+ logger.info("Ошибка обработки");
+ }
+ })
+ .listener(new ChunkListener() {
+ public void beforeChunk(@NonNull ChunkContext chunkContext) {
+ logger.info("Начало пачки");
+ }
+
+ public void afterChunk(@NonNull ChunkContext chunkContext) {
+ logger.info("Конец пачки");
+ }
+
+ public void afterChunkError(@NonNull ChunkContext chunkContext) {
+ logger.info("Ошибка пачки");
+ }
+ })
+// .taskExecutor(new SimpleAsyncTaskExecutor())
+ .build();
+ }
+
+ @Bean
+ public Step cleanUpStep() {
+ return new StepBuilder("cleanUpStep", jobRepository)
+ .tasklet(cleanUpTasklet(), platformTransactionManager)
+ .build();
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java
new file mode 100644
index 00000000..b95cb1ff
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/model/Person.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springbatch.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class Person {
+ private String name;
+ private int age;
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java
new file mode 100644
index 00000000..9b88036d
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/CleanUpService.java
@@ -0,0 +1,16 @@
+package ru.otus.example.springbatch.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class CleanUpService {
+
+ @SuppressWarnings("unused")
+ public void cleanUp() throws Exception {
+ log.info("Выполняю завершающие мероприятия...");
+ Thread.sleep(1000);
+ log.info("Завершающие мероприятия закончены");
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java
new file mode 100644
index 00000000..00ec7287
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springbatch.service;
+
+import org.springframework.stereotype.Service;
+import ru.otus.example.springbatch.model.Person;
+
+@Service
+public class HappyBirthdayService {
+
+ public Person doHappyBirthday(Person person){
+ person.setAge(person.getAge() + 1);
+ return person;
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java
new file mode 100644
index 00000000..a8f9db2c
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java
@@ -0,0 +1,61 @@
+package ru.otus.example.springbatch.shell;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.launch.JobOperator;
+import org.springframework.shell.standard.ShellComponent;
+import org.springframework.shell.standard.ShellMethod;
+import ru.otus.example.springbatch.config.AppProps;
+
+import java.util.Properties;
+
+import static ru.otus.example.springbatch.config.JobConfig.IMPORT_USER_JOB_NAME;
+import static ru.otus.example.springbatch.config.JobConfig.INPUT_FILE_NAME;
+import static ru.otus.example.springbatch.config.JobConfig.OUTPUT_FILE_NAME;
+
+@RequiredArgsConstructor
+@ShellComponent
+public class BatchCommands {
+
+ private final AppProps appProps;
+ private final Job importUserJob;
+
+ private final JobLauncher jobLauncher;
+ private final JobOperator jobOperator;
+ private final JobExplorer jobExplorer;
+
+ //http://localhost:8080/h2-console/
+
+
+ @SuppressWarnings("unused")
+ @ShellMethod(value = "startMigrationJobWithJobLauncher", key = "sm-jl")
+ public void startMigrationJobWithJobLauncher() throws Exception {
+ JobExecution execution = jobLauncher.run(importUserJob, new JobParametersBuilder()
+ .addString(INPUT_FILE_NAME, appProps.getInputFile())
+ .addString(OUTPUT_FILE_NAME, appProps.getOutputFile())
+ .toJobParameters());
+ System.out.println(execution);
+ }
+
+ @SuppressWarnings("unused")
+ @ShellMethod(value = "startMigrationJobWithJobOperator", key = "sm-jo")
+ public void startMigrationJobWithJobOperator() throws Exception {
+ Properties properties = new Properties();
+ properties.put(INPUT_FILE_NAME, appProps.getInputFile());
+ properties.put(OUTPUT_FILE_NAME, appProps.getOutputFile());
+
+ Long executionId = jobOperator.start(IMPORT_USER_JOB_NAME, properties);
+ System.out.println(jobOperator.getSummary(executionId));
+ }
+
+ @SuppressWarnings("unused")
+ @ShellMethod(value = "showInfo", key = "i")
+ public void showInfo() {
+ System.out.println(jobExplorer.getJobNames());
+ System.out.println(jobExplorer.getLastJobInstance(IMPORT_USER_JOB_NAME));
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/main/resources/application.yml b/2025-05/spring-26-spring-batch/src/main/resources/application.yml
new file mode 100644
index 00000000..2c5a9894
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/main/resources/application.yml
@@ -0,0 +1,60 @@
+spring:
+ main:
+ allow-circular-references: true
+
+ batch:
+ job:
+ enabled: false
+
+ shell:
+ interactive:
+ enabled: true
+ noninteractive:
+ enabled: false
+
+ command:
+ version:
+ enabled: false
+
+ data:
+ mongodb:
+ host: localhost
+ port: 0
+ database: SpringBatchExampleDB
+
+ datasource:
+ url: jdbc:h2:mem:testdb
+ driverClassName: org.h2.Driver
+ username: sa
+ password:
+
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+de:
+ flapdoodle:
+ mongodb:
+ embedded:
+ version: 6.0.5
+
+mongock:
+ runner-type: "InitializingBean"
+ change-logs-scan-package:
+ - ru.otus.example.springbatch.chandgelogs
+ mongo-db:
+ write-concern:
+ journal: false
+ read-concern: local
+
+app:
+ ages-count-to-add: 1
+ input-file: entries.csv
+ output-file: output.dat
+
+logging:
+ level:
+ root: ERROR
+ Batch: INFO
+ ru.otus.example.springbatch: INFO
+
diff --git a/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java b/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java
new file mode 100644
index 00000000..a470d8ae
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java
@@ -0,0 +1,71 @@
+package ru.otus.example.springbatch.config;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.test.JobLauncherTestUtils;
+import org.springframework.batch.test.JobRepositoryTestUtils;
+import org.springframework.batch.test.context.SpringBatchTest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.io.File;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static ru.otus.example.springbatch.config.JobConfig.IMPORT_USER_JOB_NAME;
+import static ru.otus.example.springbatch.config.JobConfig.INPUT_FILE_NAME;
+import static ru.otus.example.springbatch.config.JobConfig.OUTPUT_FILE_NAME;
+
+@SpringBootTest
+@SpringBatchTest
+class ImportUserJobTest {
+
+ private static final String TEST_INPUT_FILE_NAME = "test-entries.csv";
+ private static final String EXPECTED_OUTPUT_FILE_NAME = "expected-test-output.dat";
+ private static final String TEST_OUTPUT_FILE_NAME = "test-output.dat";
+
+ @Autowired
+ private JobLauncherTestUtils jobLauncherTestUtils;
+
+ @Autowired
+ private JobRepositoryTestUtils jobRepositoryTestUtils;
+
+ @BeforeEach
+ void clearMetaData() {
+ jobRepositoryTestUtils.removeJobExecutions();
+ }
+
+ @Test
+ void testJob() throws Exception {
+ var classLoader = ImportUserJobTest.class.getClassLoader();
+ var testInputFileName = URLDecoder.decode(
+ Objects.requireNonNull(classLoader.getResource(TEST_INPUT_FILE_NAME)).getFile(),
+ StandardCharsets.UTF_8
+ );
+ var expectedResultFileName = URLDecoder.decode(
+ Objects.requireNonNull(classLoader.getResource(EXPECTED_OUTPUT_FILE_NAME)).getFile(),
+ StandardCharsets.UTF_8
+ );
+
+ Job job = jobLauncherTestUtils.getJob();
+ assertThat(job).isNotNull()
+ .extracting(Job::getName)
+ .isEqualTo(IMPORT_USER_JOB_NAME);
+
+ JobParameters parameters = new JobParametersBuilder()
+ .addString(INPUT_FILE_NAME, testInputFileName)
+ .addString(OUTPUT_FILE_NAME, TEST_OUTPUT_FILE_NAME)
+ .toJobParameters();
+ JobExecution jobExecution = jobLauncherTestUtils.launchJob(parameters);
+
+ assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo("COMPLETED");
+ assertThat(new File(TEST_OUTPUT_FILE_NAME))
+ .hasSameTextualContentAs(new File(expectedResultFileName), StandardCharsets.UTF_8);
+ }
+}
\ No newline at end of file
diff --git a/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java b/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java
new file mode 100644
index 00000000..bacb4ee2
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/test/java/ru/otus/example/springbatch/testchangelogs/InitMongoDBDataChangeLog.java
@@ -0,0 +1,45 @@
+package ru.otus.example.springbatch.testchangelogs;
+
+import com.github.cloudyrock.mongock.ChangeLog;
+import com.github.cloudyrock.mongock.ChangeSet;
+import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate;
+import com.mongodb.client.MongoDatabase;
+import ru.otus.example.springbatch.model.Person;
+
+@ChangeLog(order = "001")
+public class InitMongoDBDataChangeLog {
+
+ @ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true)
+ public void dropDB(MongoDatabase database){
+ database.drop();
+ }
+
+ @ChangeSet(order = "001", id = "initPersons", author = "stvort", runAlways = true)
+ public void initPersons(MongockTemplate template){
+ template.save(new Person("Тестовый Джон", 21));
+ template.save(new Person("Тестовый Игорь", 32));
+ template.save(new Person("Тестовый Дмитрий", 52));
+ template.save(new Person("Тестовый Михаил", 22));
+ template.save(new Person("Тестовый Герман", 33));
+ template.save(new Person("Тестовый Джон", 21));
+ template.save(new Person("Тестовый Игорь", 32));
+ template.save(new Person("Тестовый Дмитрий", 52));
+ template.save(new Person("Тестовый Михаил", 22));
+ template.save(new Person("Тестовый Герман", 33));
+ template.save(new Person("Тестовый Джон", 21));
+ template.save(new Person("Тестовый Игорь", 32));
+ template.save(new Person("Тестовый Дмитрий", 52));
+ template.save(new Person("Тестовый Михаил", 22));
+ template.save(new Person("Тестовый Герман", 33));
+ template.save(new Person("Тестовый Джон", 21));
+ template.save(new Person("Тестовый Игорь", 32));
+ template.save(new Person("Тестовый Дмитрий", 52));
+ template.save(new Person("Тестовый Михаил", 22));
+ template.save(new Person("Тестовый Герман", 33));
+ template.save(new Person("Тестовый Джон", 21));
+ template.save(new Person("Тестовый Игорь", 32));
+ template.save(new Person("Тестовый Дмитрий", 52));
+ template.save(new Person("Тестовый Михаил", 22));
+ template.save(new Person("Тестовый Герман", 33));
+ }
+}
diff --git a/2025-05/spring-26-spring-batch/src/test/resources/application.yml b/2025-05/spring-26-spring-batch/src/test/resources/application.yml
new file mode 100644
index 00000000..0f1636bc
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/test/resources/application.yml
@@ -0,0 +1,43 @@
+spring:
+ main:
+ allow-circular-references: true
+
+ batch:
+ job:
+ enabled: false
+
+ shell:
+ interactive:
+ enabled: false
+ command:
+ version:
+ enabled: false
+
+ datasource:
+ url: jdbc:h2:mem:testdb
+ driverClassName: org.h2.Driver
+ username: sa
+ password:
+
+
+
+ data:
+ mongodb:
+ host: localhost
+ port: 0
+ database: SpringBatchTestExampleDB
+
+de:
+ flapdoodle:
+ mongodb:
+ embedded:
+ version: 6.0.5
+
+mongock:
+ runner-type: "InitializingBean"
+ change-logs-scan-package:
+ - ru.otus.example.springbatch.testchangelogs
+ mongo-db:
+ write-concern:
+ journal: false
+ read-concern: local
\ No newline at end of file
diff --git a/2025-05/spring-26-spring-batch/src/test/resources/expected-test-output.dat b/2025-05/spring-26-spring-batch/src/test/resources/expected-test-output.dat
new file mode 100644
index 00000000..b4c926b1
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/test/resources/expected-test-output.dat
@@ -0,0 +1,16 @@
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
+Person(name=Ivan, age=24)
+Person(name=Mary, age=25)
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
+Person(name=Sunny, age=24)
+Person(name=John, age=25)
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
+Person(name=Ivan, age=24)
+Person(name=John, age=25)
diff --git a/2025-05/spring-26-spring-batch/src/test/resources/test-entries.csv b/2025-05/spring-26-spring-batch/src/test/resources/test-entries.csv
new file mode 100644
index 00000000..bde433ce
--- /dev/null
+++ b/2025-05/spring-26-spring-batch/src/test/resources/test-entries.csv
@@ -0,0 +1,16 @@
+Ivan,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+Mary,24
+Ivan,23
+John,24
+Sunny,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+John,24
+Ivan,23
+John,24