Spring Batch5 Junit5(テストコード)実装

# 内容

Batchのテストコード実装しました。
テスト内容については説明は記載していません。
Spring Batchのテストで使われるアノテーションについて少し説明を記載しています。
なのでテスト自動化はやったことあるけどSpring Batchだとどうやるんだという人向けになってます。

# git

https://github.com/wow74/spring-batch-multi-file
# ファイル構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604560/e59ff6c3-a39c-412e-a02b-65fce29d2ca1.png)

# build.gradle

``` java:build.gradle
plugins {
  id 'java'
  id 'org.springframework.boot' version '3.4.0'
  id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(23)
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-batch'
  runtimeOnly 'com.h2database:h2'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.springframework.batch:spring-batch-test'
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
  // プロダクトコード用
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'

  // テストコード用
  testCompileOnly 'org.projectlombok:lombok'
  testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
  useJUnitPlatform()
}

```

# プロダクトコード

前回投稿した下記記事のコードを少し改良したものを使ってます。
コードはほぼ同じです。
https://qiita.com/_wow_/items/008ad4e304cfde4ab35c

<details><summary>プロダクトコード</summary>

## Resouces

### csv

``` csv:female.csv
id,name
2,hanako
3,hana
```

``` csv:male.csv
id,name
1,taro
```

### property

``` demo.properties
csv.file.path=classpath:csv/*.csv
```

## Chunk

### CsvReader

``` java:CsvReader.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.MultiResourceItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class CsvReader {

  private Resource[] getCsvFiles(final String filePath) throws IOException {
    return new PathMatchingResourcePatternResolver().getResources(filePath);
  }

  public MultiResourceItemReader<User> read(final String filePath) throws IOException {
    System.out.println("read");
    final MultiResourceItemReader<User> multiReader = new MultiResourceItemReader<>();
    multiReader.setResources(getCsvFiles(filePath));
    multiReader.setDelegate(singleFileReader());

    return multiReader;
  }

  private FlatFileItemReader<User> singleFileReader() {
    final String[] nameArray = new String[]{"id", "name"};
    return new FlatFileItemReaderBuilder<User>()
            .name("csvReader")
            .linesToSkip(1)
            .encoding(StandardCharsets.UTF_8.name())
            .delimited()
            .names(nameArray)
            .fieldSetMapper(new BeanWrapperFieldSetMapper<User>() {{
              setTargetType(User.class);
            }})
            .build();
  }
}
```

### CsvProcessor

``` java:CsvProcessor.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;

@Component
@StepScope
@Slf4j
public class CsvProcessor implements ItemProcessor<User, User> {

  @Override
  public User process(final User item) throws Exception {
    try {
      System.out.println("process");
      item.setName(item.getName() + "さん");
      System.out.println(item.toString());
    } catch (Exception e) {
      log.warn(e.getMessage(), e);
      return null;
    }
    return item;
  }
}
```

### CsvWriter

``` java:CsvWriter.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

@Component
@StepScope
public class CsvWriter implements ItemWriter<User> {

  @Autowired
  private UserRepository userRepository;

  @Override
  public void write(final Chunk<? extends User> items) throws Exception {
    System.out.println("write");
    userRepository.dummyBulkInsert(new ArrayList<>(items.getItems()));
    System.out.println("登録完了");
  }
}
```

## Model

### User

``` java:User.java
package com.example.demo.model;

import lombok.Data;
import org.springframework.batch.item.ResourceAware;
import org.springframework.core.io.Resource;

@Data
public class User implements ResourceAware {
  private long id;
  private String name;
  private String fileName;

  @Override
  public void setResource(final Resource resource) {
    fileName = resource.getFilename();
  }
}
```

## Repository

### UserRepository

``` java:UserRepository.java
package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class UserRepository {

  public int dummyBulkInsert(List<User> user) {
    // 実際のインサート処理を記述する
    return user.size();
  }
}
```

今回はテストに重きを置きたかったのでDB周りは仮の処理を実装しています。
上記はバルクインサートを行うクエリが呼ばれているとして読んでください。

## Resource

### CsvResource

``` java:CsvResource.java
package com.example.demo.resource;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CsvResource {
  public Resource[] getResources(String filePath) throws IOException {
    return new PathMatchingResourcePatternResolver().getResources(filePath);
  }
}
```

## Tasklet

ファイル存在チェックを2種類のやり方でやっています。
どちらも結果は同じになりますがtasklet1はリソースを外部で取得するようにしているため
リソース取得の処理をモック化できます。

### CsvExistsCheckTasklet1

``` java:CsvExistsCheckTasklet1
package com.example.demo.tasklet;

import com.example.demo.resource.CsvResource;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.io.FileNotFoundException;
import java.io.IOException;

@Component
@StepScope
@PropertySource("classpath:property/demo.properties")
public class CsvExistsCheckTasklet1 implements Tasklet {

  @Value("${csv.file.path}")
  private String filePath;

  @Autowired
  private  CsvResource csvResource;

  @Override
  public RepeatStatus execute(StepContribution stepContribution, ChunkContext context) throws IOException {
    System.out.println("tasklet1");
    final Resource[] resources = csvResource.getResources(filePath);
    if (resources.length == 0) throw new FileNotFoundException("csvファイルが見つかりませんでした");
    return RepeatStatus.FINISHED;
  }

}
```

### CsvExistsCheckTasklet2

``` java:CsvExistsCheckTasklet2
package com.example.demo.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;

import java.io.FileNotFoundException;
import java.io.IOException;

@Component
@StepScope
@PropertySource("classpath:property/demo.properties")
public class CsvExistsCheckTasklet2 implements Tasklet {

  @Value("${csv.file.path}")
  private String filePath;

  @Override
  public RepeatStatus execute(StepContribution stepContribution, ChunkContext context) throws IOException {
    System.out.println("tasklet2");
    final Resource[] resources = new PathMatchingResourcePatternResolver().getResources(filePath);
    if (resources.length == 0) throw new FileNotFoundException("csvファイルが見つかりませんでした");
    return RepeatStatus.FINISHED;
  }

}
```

## BatchConfig

``` java:BatchConfig.java
package com.example.demo;

import com.example.demo.chunk.CsvProcessor;
import com.example.demo.chunk.CsvReader;
import com.example.demo.chunk.CsvWriter;
import com.example.demo.model.User;
import com.example.demo.tasklet.CsvExistsCheckTasklet1;
import com.example.demo.tasklet.CsvExistsCheckTasklet2;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
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.step.builder.StepBuilder;
import org.springframework.batch.item.file.MultiResourceItemReader;
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.context.annotation.PropertySource;
import org.springframework.transaction.PlatformTransactionManager;

import java.io.IOException;
import java.io.PrintStream;

@Configuration
@PropertySource("classpath:property/demo.properties")
public class BatchConfig {

  @Autowired
  private CsvExistsCheckTasklet1 tasklet;

  @Autowired
  private CsvReader reader;

  @Autowired
  private CsvProcessor processor;

  @Autowired
  private CsvWriter writer;

  @Bean
  @StepScope
  public MultiResourceItemReader<User> read(@Value("${csv.file.path}") final String filePath) throws IOException {
    return reader.read(filePath);
  }

  @Bean
  public Step fileCheckStep(final JobRepository jobRepository, final PlatformTransactionManager transactionManager) {
    return new StepBuilder("fileCheckStep", jobRepository)
            .tasklet(tasklet, transactionManager)
            .build();
  }

  @Bean
  public Step demoStep(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, final MultiResourceItemReader<User> read) throws IOException {
    System.out.println("step");
    return new StepBuilder("demoStep", jobRepository)
            .<User, User>chunk(10, transactionManager)
//            .reader(read(null)) // 引数でreadをDIしない場合はこちらを使用する
            .reader(read)
            .processor(processor)
            .writer(writer)
            .build();
  }

  @Bean
  public Job demoJob(final JobRepository jobRepository, final PlatformTransactionManager transactionManager,
                     final Step fileCheckStep, final Step demoStep) throws Exception {
    System.out.println("job");
    System.setOut(new PrintStream(System.out, true, "UTF-8"));
    return new JobBuilder("demoJob", jobRepository)
            .incrementer(new RunIdIncrementer())
            .start(fileCheckStep)
            .next(demoStep)
            .build();
  }
}
```
readメソッドの引数にvalueアノテーションをつけることでプロパティファイルの値を読み取るようになっています。フィールドに持たせるとreadメソッドのDIに失敗します。

## DemoApplication

``` java:DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}
```
</details>


---


# テストコード

## Resouces

### csv

プロダクトコードのほうと同じです。
パスは異なります。

### property

``` demo.properties
csv.file.path=classpath:test/csv/*.csv
```

## Chunk

### CsvReaderTest

``` java:CsvReaderTest.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.item.file.MultiResourceItemReader;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvReaderTest")
public class CsvReaderTest {

  @InjectMocks
  private CsvReader csvReader;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvReaderTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvReaderTest 終了");
  }

  @Test
  @DisplayName("readTest")
  public void readTest() throws Exception {
    // SETUP
    log.info("readTest 開始");
    final User[] expectedArray = {
            createUser(2L, "hanako", "female.csv"),
            createUser(3L, "hana", "female.csv"),
            createUser(1L, "taro", "male.csv")
    };

    // WHEN
    final MultiResourceItemReader<User> reader = csvReader.read("classpath:test/csv/*.csv");

    assertEquals(expectedArray[0], reader.read());
    assertEquals(expectedArray[1], reader.read());
    assertEquals(expectedArray[2], reader.read());
    assertNull(reader.read());

    // THEN
    log.info("readTest 終了");
  }

  private User createUser(final long id, final String name, final String fileName) {
    final User user = new User();
    user.setId(id);
    user.setName(name);
    user.setFileName(fileName);
    return user;
  }

}
```

@ExtendWith(MockitoExtension.class)を使ってテストをすることでspring bootを起動させずにテストをします。これでspring bootの起動を待つ必要がなくなります。

@AutowiredのようなDIが必要なければ上記アノテーションでテストをします。

### CsvProcessorTest

``` java:CsvProcessorTest.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvProcessorTest")
public class CsvProcessorTest {

  @InjectMocks
  private CsvProcessor csvProcessor;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvProcessorTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvProcessorTest 終了");
  }

  @Test
  @DisplayName("executeTest_noException")
  public void executeTest_noException() throws Exception {
    // SETUP
    log.info("executeTest_noException 開始");
    final User item = createUser(1L);
    final User expected = createUser(1L);
    expected.setName(expected.getName() + "さん");

    // WHEN
    final User actual = csvProcessor.process(item);

    // THEN
    assertEquals(expected, actual);

    log.info("executeTest_noException 終了");

  }

  @Test
  @DisplayName("executeTest_exception")
  public void executeTest_exceptionCheck() throws Exception {
    // SETUP
    log.info("executeTest_exception 開始");

    // WHEN
    final User actual = csvProcessor.process(null);

    // THEN
    assertNull(actual);

    log.info("executeTest_exception 終了");

  }

  private User createUser(final long id) {
    final User user = new User();
    user.setId(id);
    user.setName("name" + id);
    user.setFileName("file" + id);
    return user;
  }
}
```

### CsvWriterTest

``` java:CsvWriterTest.java
package com.example.demo.chunk;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.item.Chunk;

import java.io.ByteArrayOutputStream;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvWriterTest")
public class CsvWriterTest {

  @InjectMocks
  private CsvWriter csvWriter;

  @Mock
  private UserRepository userRepository;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvWriterTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvWriterTest 終了");
  }

  @Test
  @DisplayName("write")
  public void write() throws Exception {
    // SETUP
    log.info("CsvWriterTest 開始");
    final Chunk<User> items = new Chunk<>(new User(), new User());

    // WHEN
    csvWriter.write(items);

    // THEN
    verify(userRepository, times(1)).dummyBulkInsert(any());

    log.info("CsvWriterTest 終了");
  }
}
```

## Repository

### UserRepositoryTest

``` java:UserRepositoryTest.java
package com.example.demo.repository;

import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
//import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertIterableEquals;

// @SpringBatchTest // SpringBatchの機能(Job, Step)をテストするときに使うアノテーション
@SpringBootTest // RepositoryはBatch機能ではないためSpringBootTestアノテーションを使用
@Slf4j
@DisplayName("UserRepositoryTest")
@Transactional
class UserRepositoryTest {

  @Autowired
  private UserRepository userRepository;

  @BeforeAll
  public static void beginTest() {
    log.info("UserRepositoryTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("UserRepositoryTest 終了");
  }

  @Test
  @DisplayName("dummyBulkInsertTest")
  public void dummyBulkInsertTest() {
    // SETUP
    log.info("dummyBulkInsertTest 開始");
    final List<User> expected = List.of(createUserData(1L), createUserData(2L));

    // WHEN
    userRepository.dummyBulkInsert(expected);

    // THEN
    // List<User> actual = xxx.selectByExample();
    final List<User> actual = expected; // selectでデータを取得したとする

    assertIterableEquals(expected, actual);

    // CLEANUP
    // @Transactionalでロールバック実行
    log.info("dummyBulkInsertTest 終了");

  }

  private User createUserData(final long id) {
    final User user = new User();
    user.setId(id);
    user.setName("name" + id);
    user.setFileName("fileName" + id);
    return user;
  }

}
```

DIが必要な場合は@SpringBootTestを使ってテストをします。

## Resource

### CsvResourceTest

``` java:CsvResourceTest.java
package com.example.demo.resource;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;

@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvReaderTest")
public class CsvResourceTest {

  @InjectMocks
  private CsvResource csvResource;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvResourceTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvResourceTest 終了");
  }

  @Test
  @DisplayName("getResourcesTest")
  public void getResourcesTest() throws IOException {
    // SETUP
    final String filePath = "classpath:test/csv/*.csv";
    final String[] expected = {"female.csv", "male.csv"};

    // WHEN
    final Resource[] resources = csvResource.getResources(filePath);
    final String[] actual = Arrays.stream(resources).map(Resource::getFilename).toArray(String[]::new);

    // THEN
    assertArrayEquals(expected, actual);
    log.info("getResourcesTest 開始");
  }
}
```

## Tasklet

### CsvExistsCheckTasklet1Test

``` java:CsvExistsCheckTasklet1Test.java
package com.example.demo.tasklet;

import com.example.demo.resource.CsvResource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.core.io.Resource;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.FileNotFoundException;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

// @ExtendWith(クラス)でテストに使用するクラスを指定できる
// MockitoExtensionを指定するとMockitoテストが可能になる(モックが使える)
// DIしたくないときに使える
@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvExistsCheckTaskletTest")
public class CsvExistsCheckTaskletTest1 {

  @InjectMocks // モックを使いたいクラスに付与することで@MockをDIしてくれる
  private CsvExistsCheckTasklet1 csvExistsCheckTasklet;

  @Mock // テスト元でDIしているものをモック化する
  private CsvResource csvResource;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvExistsCheckTaskletTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvExistsCheckTaskletTest 終了");
  }

  @Test
  @DisplayName("executeTest_existsFile")
  public void executeTest_success() throws IOException {
    // SETUP
    log.info("executeTest_existsFile 開始");
    ReflectionTestUtils.setField(csvExistsCheckTasklet, "filePath", "", String.class);
    final RepeatStatus expected = RepeatStatus.FINISHED;

    // WHEN
    when(csvResource.getResources(anyString())).thenReturn(new Resource[]{any()});
    final RepeatStatus actual = csvExistsCheckTasklet.execute(null, null);

    // THEN
    assertEquals(expected, actual);

    log.info("executeTest_existsFile 終了");

  }

  @Test
  @DisplayName("executeTest_notExistsFile")
  public void executeTest_fail() throws IOException {
    // SETUP
    log.info("executeTest_notExistsFile 開始");
    ReflectionTestUtils.setField(csvExistsCheckTasklet, "filePath", "", String.class);

    // THEN
    when(csvResource.getResources(anyString())).thenReturn(new Resource[]{});
    assertThrows(FileNotFoundException.class, () -> csvExistsCheckTasklet.execute(null, null));
    log.info("executeTest_notExistsFile 終了");
  }
}

```

### CsvExistsCheckTasklet2Test

``` java:CsvExistsCheckTasklet2.java
package com.example.demo.tasklet;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.FileNotFoundException;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

// @ExtendWith(クラス)でテストに使用するクラスを指定できる
// MockitoExtensionを指定するとMockitoテストが可能になる(モックが使える)
// DIしたくないときに使える
@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("CsvExistsCheckTaskletTest")
public class CsvExistsCheckTaskletTest2 {

  @InjectMocks // モックを使いたいクラスに付与することで@MockをDIしてくれる
  private CsvExistsCheckTasklet2 csvExistsCheckTasklet;

//  @Mock // テスト元でDIしているものをモック化する
//  private XXX xxx;

  @BeforeAll
  public static void beginTest() {
    log.info("CsvExistsCheckTaskletTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("CsvExistsCheckTaskletTest 終了");
  }

  @Test
  @DisplayName("executeTest_existsFile")
  public void executeTest_success() throws IOException {
    // SETUP
    log.info("executeTest_existsFile 開始");
    ReflectionTestUtils.setField(csvExistsCheckTasklet, "filePath", "classpath:test/csv/*.csv", String.class);
    final RepeatStatus expected = RepeatStatus.FINISHED;

    // WHEN
    final RepeatStatus actual = csvExistsCheckTasklet.execute(null, null);

    // THEN
    assertEquals(expected, actual);

    log.info("executeTest_existsFile 終了");

  }

  @Test
  @DisplayName("executeTest_notExistsFile")
  public void executeTest_fail() throws IOException {
    // SETUP
    log.info("executeTest_notExistsFile 開始");
    ReflectionTestUtils.setField(csvExistsCheckTasklet, "filePath", "classpath:test/csv/empty/*.csv", String.class);

    // THEN
    assertThrows(FileNotFoundException.class, () -> csvExistsCheckTasklet.execute(null, null));
    log.info("executeTest_notExistsFile 終了");
  }
}

```

## BatchConfigTest

``` java:BatchConfigTest.java
package com.example.demo;

import com.example.demo.chunk.CsvProcessor;
import com.example.demo.chunk.CsvReader;
import com.example.demo.chunk.CsvWriter;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.resource.CsvResource;
import com.example.demo.tasklet.CsvExistsCheckTasklet1;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.item.file.MultiResourceItemReader;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.test.context.ContextConfiguration;

import static org.junit.jupiter.api.Assertions.*;

@SpringBatchTest
// DI対象のクラスを列挙する
@ContextConfiguration(classes = {BatchConfig.class, CsvExistsCheckTasklet1.class, CsvReader.class, CsvProcessor.class, CsvWriter.class, CsvResource.class, UserRepository.class})
@EnableAutoConfiguration // 必要そうなbeanを自動で構成してくれる
@Slf4j
@DisplayName("BatchConfigTest")
public class BatchConfigTest {


  @Autowired
  private JobLauncherTestUtils jobLauncherTestUtils;

  @Autowired
  private CsvReader csvReader;

  @BeforeAll
  public static void beginTest() {
    log.info("BatchConfigTest 開始");
  }

  @AfterAll
  public static void endTest() {
    log.info("BatchConfigTest 終了");
  }

  @Test
  @DisplayName("readTest")
  public void readTest(@Value("${csv.file.path}") String filePath) throws Exception {
    // SETUP
    log.info("readTest 開始");
//    ReflectionTestUtils.setField(csvExistsCheckTasklet, "filePath", "", String.class);
    final RepeatStatus expected = RepeatStatus.FINISHED;

    // WHEN
    MultiResourceItemReader<User> reader = csvReader.read(filePath);

    // THEN
    assertNotNull(reader.read());
    assertNotNull(reader.read());
    assertNotNull(reader.read());
    assertNull(reader.read());

    log.info("readTest 終了");

  }

  @Test
  @DisplayName("fileCheckStepTest")
  public void fileCheckStepTest() {
    // SETUP
    log.info("executeTest_existsFile 開始");

    // WHEN
    // 読み込まれたクラスのプロパティファイル読み込みはtest側が使用される
    final JobExecution jobExecution = jobLauncherTestUtils.launchStep("fileCheckStep");

    // THEN
    assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    log.info("executeTest_existsFile 終了");

  }

  @Test
  @DisplayName("demoStepTest")
  public void demoStepTest() {
    // SETUP
    log.info("demoStepTest 開始");

    // WHEN
    final JobExecution jobExecution = jobLauncherTestUtils.launchStep("demoStep");

    // THEN
    assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    log.info("demoStepTest 終了");
  }

  @Test
  @DisplayName("demoJobTest")
  public void demoJobTest() throws Exception {
    // SETUP
    log.info("demoJobTest 開始");

    // WHEN
    final JobExecution jobExecution = jobLauncherTestUtils.launchJob();

    // THEN
    assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    log.info("demoJobTest 終了");
  }


}
```
@ContextConfigurationでバッチ内でDIされているすべてのクラスを指定します。
@EnableAutoConfigurationで上記クラスをDIします。

## DemoApplicationTests

これはなんか勝手に作られてました。

``` java:DemoApplication.java
package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

	@Test
	void contextLoads() {
	}

}

```

# 参考

@Valueのような外部値をテストで扱う方法について記載

https://qiita.com/kazuki43zoo/items/0ce92fce6d6f3b7bf8eb#testpropertysource%E3%81%AE%E5%88%A9%E7%94%A8

Junitについて記載

https://qiita.com/shikanomoto/items/e1760f92b8bf67409710#repeatedtest

アサーションについて

https://qiita.com/shikanomoto/items/3cb138e43e50ade59ffd#assertarrayequalsexpected-actual

Mockitoについて

https://qiita.com/emily-08/items/0772b1ec49102a9f62a3

テスト　実行回数の検証について

https://qiita.com/unmelt/items/bb11e25bc6de7b6fe787

テスト　戻り値設定について

https://qiita.com/superigera/items/630e0270671aa372b045

Spring Boot テストアノテーションについて

https://zenn.dev/simsim/articles/4b9930dcf2308b