いまさらSpringBoot＋JUnit5でテストを書いてみた

# はじめに
JUnit4に慣れ過ぎて、いざJUnit5で書こうと思った時に結構調べる必要がありました。
今更ながら、よく使う実装サンプルを自分用にまとめておきます。

なお、テスト対象のWebアプリはRestAPIを想定しておりView層のテストは含みません。

# この記事で扱う実装サンプル

- Controllerのテスト
  MockMVCで擬似リクエストを行い Filter + Controller + ExceptionHandler をテストする

- Mockを使ったテスト
  - BeanをMock化してテストする
  - 片方をDI、もう片方をMock化してテストする

- パラメータ化テスト
  テストデータだけを変えて同じケースを繰り返しテストする

# 開発環境
- OS : macOS Catalina
- IDE : IntelliJ Ultimate
- Java : 11
- SpringBoot : 2.3.4
- Gradle : 6.6.1

# 1. テスト対象のアプリケーションの準備
まずはテスト対象となるアプリを作成します。
例によってサクっと [spring initializr で GradleProject に lombok, Spring Web, Validation をdependenciesに追加して作成](https://start.spring.io/#!type=gradle-project&language=java&platformVersion=2.3.4.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=lombok,web,validation) します。

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

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

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	testCompileOnly 'org.projectlombok:lombok'  // 追加
	testAnnotationProcessor 'org.projectlombok:lombok'  // 追加
}

test {
	useJUnitPlatform()
}
```

生成された `build.gradle` に対し、テストコード内でも `lombok` を使えるように `dependency` に２行追加しています。（上記コメント参照）
このプロジェクトに対し、次のクラスを作成していきます。

## 1-1.ServiceとConfigurationの作成
`Controller` から呼ばれる `Service` を２つ作ります。
まずはシンプルな内部処理を行うもの。

```java:DemoService.java
@Service
public class DemoService {
  // あいさつするよ
  public String hello() {
    return "hello";
  }
  // 割り算するよ
  public BigDecimal divide(BigDecimal a, BigDecimal b) {
    return a.divide(b, 2, RoundingMode.HALF_UP);
  }
}
```
もう一つは外部リソース（Qiita API）を取得するもの。

```java:ExternalService.java
@Service
@RequiredArgsConstructor
public class ExternalService {
  private static final String EXTERNAL_RESOURCE_URL = "https://qiita.com/api/v2/schema";
  private final RestTemplate restTemplate;
  // Qiita APIのSchemaの結果を返すよ
  public String getExternalResource() {
    ResponseEntity<String> response =
        restTemplate.exchange(EXTERNAL_RESOURCE_URL, HttpMethod.GET, null, String.class);
    return response.getBody();
  }
}
```

上記の `restTemplate` をDIするため `Configuration` クラスも作成しておきます。

```java:AppConfig.java
@Configuration
public class AppConfig {
  @Bean
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }
}
```

## 1-2. Controllerの作成
先ほどの２つのServiceを使うControllerを作ります。

```java:DemoController.java
@RestController
@RequiredArgsConstructor
@Validated
public class DemoController {

  private final DemoService demoService;
  private final ExternalService externalService;

  // あいさつするよ
  @GetMapping("/")
  public CommonResponse hello() {
    String data = demoService.hello();
    return CommonResponse.builder().data(data).build();
  }

  // 割り算するよ
  @GetMapping("/divide/{num1}/{num2}")
  public CommonResponse divide(
      @PathVariable @Pattern(regexp = "[0-9]*") String num1,
      @PathVariable @Pattern(regexp = "[0-9]*") String num2) {
    BigDecimal data = demoService.divide(new BigDecimal(num1), new BigDecimal(num2));
    return CommonResponse.builder().data(data).build();
  }

  // Qiita APIのSchemaの結果を返すよ
  @GetMapping("/external")
  public CommonResponse external() {
    String data = externalService.getExternalResource();
    return CommonResponse.builder().data(data).build();
  }
}
```
割り算の方は入力チェックをしているので、この観点で後ほどテストケースを実装します。
レスポンスクラスも以下のように作成します。

```java:CommonResponse.java
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CommonResponse<T> {
    @Builder.Default
    private String status = "success";
    @Builder.Default
    private String message = "request succeeded.";
    private T data;
}
```

## 1-3. FilterとExceptionHandlerの作成
より実践的なサンプルにするためFilterとExceptionHandlerも作成します。
このフィルタはリクエスト処理前後にログを出力するだけのシンプルなものです。

```java:LogFilter.java
@Component
@Slf4j
public class LogFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    log.info("[IN]{}:{}", req.getMethod(), req.getRequestURI());
    try {
      chain.doFilter(request, response);
    } finally {
      log.info("[OUT]{}:{}", req.getMethod(), req.getRequestURI());
    }
  }
}
```

ExceptionHandlerも作成します。
各種エラーを共通で処理するために使います。

```java:CommonExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler extends ResponseEntityExceptionHandler {

  // 404: Resource Not Foundエラーを処理するよ
  // ※ これをハンドリングするには application.properties の設定も必要だよ
  @Override
  protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    ServletWebRequest req = (ServletWebRequest)request;
    log.warn("resource not found. {}", req.getRequest().getRequestURI());
    return new ResponseEntity(
            CommonResponse.builder().status("failure").message("resource not found.").build(),
            HttpStatus.NOT_FOUND);
  }

  // 400: 入力チェックエラーを処理するよ
  @ExceptionHandler(ConstraintViolationException.class)
  public ResponseEntity<CommonResponse> handleValidationError(ConstraintViolationException e) {
    // 入力エラー項目とメッセージをカンマ区切り(,)に追加。
    String validationErrorMessages =
        e.getConstraintViolations().stream()
            .map(cv -> cv.getPropertyPath().toString() + ":" + cv.getMessage())
            .collect(Collectors.joining(", "));
    log.info("Bad request. {}", validationErrorMessages);
    return new ResponseEntity<>(
        CommonResponse.builder().status("failure").message(validationErrorMessages).build(),
        HttpStatus.BAD_REQUEST);
  }

  // 500: それ以外の不明なエラーを処理するよ
  @ExceptionHandler
  public ResponseEntity<CommonResponse> handleException(Exception e) {
    log.error("Request failed.", e);
    return new ResponseEntity<>(
        CommonResponse.builder().status("failure").message("error has occurred.").build(),
        HttpStatus.INTERNAL_SERVER_ERROR);
  }
}
```
上記のExceptionHandlerで `404 : Resouce Not Found` をハンドリングするために `application.properties` に以下の設定を入れます。

```properties:application.properties
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
```

## 1-4. 作成したファイルの構成
以上でテストに使うアプリケーションの準備は完了です。
ここまでで編集・作成したファイルは以下の構成になっています。

```bash
├── build.gradle
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.example.demo
│   │   │               ├── AppConfig.java
│   │   │               ├── DemoApplication.java
│   │   │               ├── controller
│   │   │               │   ├── CommonExceptionHandler.java
│   │   │               │   ├── CommonResponse.java
│   │   │               │   └── DemoController.java
│   │   │               ├── filter
│   │   │               │   └── LogFilter.java
│   │   │               └── service
│   │   │                   ├── DemoService.java
│   │   │                   └── ExternalService.java
│   │   └── resources
│   │       └── application.properties
│   └── test
└── web
```

#2. Controllerのテスト
Controllerの主な役割は以下の通りです。

- リクエストのマッピング
- パラメータの取得
- 入力チェック
- ビジネスロジック（Service）の呼び出し
- レスポンスの返却

これらの動作はSpringMVCの機能に依存しているため、Controllerクラス単体のテストコードを書いてもあまり意味がありません。
そこで `MockMVC` を使ってSpringMVCの動作を再現したテストを行います。

## 2-1. MockMVCを使ったテストケースのサンプル
Controllerが受け付けるリクエストについて正常ケースだけでなく、入力チェックエラーなど想定される異常ケースのレスポンスももれなく検証を行います。


```java:DemoControllerTest.java
package com.example.demo.controller;

import com.example.demo.filter.LogFilter;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@EnableWebMvc
@Slf4j
public class DemoControllerTest {

  MockMvc mockMvc;

  @Autowired WebApplicationContext webApplicationContext;
  @Autowired LogFilter logFilter;

  @BeforeEach
  void beforeEach() {
    mockMvc =
        MockMvcBuilders.webAppContextSetup(webApplicationContext)     // MockMVCをセットアップ
            .addFilter(logFilter, "/*")                               // ただしfilterは手動で追加が必要
            .build();
  }

  // ルート「/」のリクエストをテストするよ
  @Test
  void hello() throws Exception {
    mockMvc.perform(get("/"))                                         // ルート「/」に擬似リクエスト送信
        .andExpect(status().isOk())                                   // HttpStatus が 200:OK であること
        .andExpect(jsonPath("$.status").value("success"))             // jsonの値が期待値通りであること
        .andExpect(jsonPath("$.message").value("request succeeded.")) // 〃
        .andExpect(jsonPath("$.data").value("hello"));
  }

  // 除算（10 ÷ 3）のリクエストをテストするよ
  @Test
  void divideSuccess() throws Exception { 
    mockMvc
        .perform(get("/divide/10/3"))                                 // 「/divide/10/3」に擬似リクエスト送信
        .andExpect(status().isOk())                                   // HttpStatus が 200:OK であること
        .andExpect(jsonPath("$.status").value("success"))
        .andExpect(jsonPath("$.message").value("request succeeded."))
        .andExpect(jsonPath("$.data").value("3.33"));                 // 10 ÷ 3 = 3.33 であること
  }
  
  // 不正なリクエスト（10 ÷ aaa）をテストするよ
  @Test
  void divideInvalidParameter() throws Exception {
    mockMvc
        .perform(get("/divide/10/aaa"))                               // 「/divide/10/aaa」に擬似リクエスト送信
        .andExpect(status().isBadRequest())                           // HttpStatus が 400:BadRequest であること
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("divide.num2:must match \"[0-9]*\"")); // エラーメッセージがあること
  }

  // ゼロ除算（10 ÷ 0）のリクエストをテストするよ
  @Test
  void divideZeroError() throws Exception {
    mockMvc
        .perform(get("/divide/10/0"))                                   // 「/divide/10/0」に擬似リクエスト送信 
        .andExpect(status().is5xxServerError())                         // HttpStatus が 500:ServerError
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("error has occurred."));
  }
  
  // 外部リソース（Qiita schema API）の取得をテストするよ
  @Test
  void getExternalResource() throws Exception {
    MvcResult mvcResult =
        mockMvc
            .perform(get("/external"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("success"))
            .andExpect(jsonPath("$.message").value("request succeeded."))
            .andExpect(jsonPath("$.data").isNotEmpty())                // 空じゃないこと
            .andReturn();
    // 取得したレスポンスをログに出力しておくよ
    log.info("external response : {}", mvcResult.getResponse().getContentAsString());
  }
}
```
ポイントは `MockMvc` を `WebAppicationContext` を使ってセットアップすることです。[^1]
こうすることでアプリケーションサーバーにデプロイしたのとほぼ同等な状態を再現します。

[^1]: MockMvcにはもう一つ `standalone` セットアップもあり、テスト対象のControllerやControllerAdvice、Configなど細かくカスタマイズできる単体テスト向きなモードもあります。

**ただし `Filter` だけは自前で `addFilter(Filter, "path")` で指定してあげる必要がある点に注意。**



#3. Mockを使ったテスト
先ほどのテストはDIされたControllerやServiceを利用していました。

しかし `ExternalService` のように外部リソースを利用する場合、その状態（相手のサーバーがダウンしてアクセスできない、Wi-FiがOFFになっている、など）によってControllerのテストが失敗するのはよくありません。

**そんなときにMockを使います。**

##3-1. BeanをMock化してテストする
もう一度、Controllerの役割を確認すると、

1. リクエストのマッピング
2. パラメータの取得
3. 入力チェック
4. ビジネスロジック（Service）の呼び出し
5. レスポンスの返却

ですが、このうち `4. ビジネスロジック（Service）の呼び出し` は、 **期待値を返すようモック化することでControllerのテストに集中できる** ようになります。

先ほどのテストのServiceクラスをMock化してみます。

```java:DemoControllerWithMockTest.java
package com.example.demo.controller;

import com.example.demo.filter.LogFilter;
import com.example.demo.service.DemoService;
import com.example.demo.service.ExternalService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import java.math.BigDecimal;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@EnableWebMvc
@Slf4j
class DemoControllerWithMockTest {

  MockMvc mockMvc;

  @Autowired WebApplicationContext webApplicationContext;
  @Autowired LogFilter logFilter;

  @MockBean DemoService demoService;         // Mock化してDIコンテナに登録する
  @MockBean ExternalService externalService; // 〃

  @BeforeEach
  void beforeEach() {
    MockitoAnnotations.initMocks(this);
    mockMvc =
        MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .addFilter(logFilter, "/*")
            .build();
  }

  @AfterEach
  void afterEach() {}

  @Test
  void hello() throws Exception {
    // mock
    when(demoService.hello()).thenReturn("こんにちは");      // 最初にモックの戻り値をセット
    // request execute
    mockMvc
        .perform(get("/"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("success"))
        .andExpect(jsonPath("$.message").value("request succeeded."))
        .andExpect(jsonPath("$.data").value("こんにちは"));  // 期待値も変更して検証
    // verify
    verify(demoService, times(1)).hello();                 // モックの呼び出し回数を検証
  }

  @Test
  void divideSuccess() throws Exception {
    // mock
    when(demoService.divide(any(), any())).thenReturn(new BigDecimal("3.33")); // 引数関係なく"3.33"を返す
    // request execute
    mockMvc
        .perform(get("/divide/10/3"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("success"))
        .andExpect(jsonPath("$.message").value("request succeeded."))
        .andExpect(jsonPath("$.data").value("3.33"));
    // verify
    verify(demoService, times(1)).divide(any(), any());
  }

  @Test
  void divideInvalidParameter() throws Exception {
    // request execute
    mockMvc
        .perform(get("/divide/10/aaa"))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("divide.num2:must match \"[0-9]*\""));
    // verify
    verify(demoService, times(0)).divide(any(), any());  // 入力エラーのためモックの呼び出しは0回を検証
  }

  @Test
  void divideZeroError() throws Exception {
    // mock
    when(demoService.divide(any(), eq(BigDecimal.ZERO)))
        .thenThrow(new ArithmeticException("/ by zero"));              // ゼロ除算を想定してエラーを再現
    // request execute
    mockMvc
        .perform(get("/divide/10/0"))
        .andExpect(status().is5xxServerError())
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("error has occurred."));
    // verify
    verify(demoService, times(1)).divide(any(), eq(BigDecimal.ZERO));  // モックの呼び出しは1回を検証
  }

  @Test
  void getExternalResource() throws Exception {
    // mock
    when(externalService.getExternalResource())
        .thenReturn("this is mock data for internal test.");    // 外部リソースアクセスせずに文言返す
    // request execute
    MvcResult mvcResult =
        mockMvc
            .perform(get("/external"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("success"))
            .andExpect(jsonPath("$.message").value("request succeeded."))
            .andExpect(jsonPath("$.data").value("this is mock data for internal test."))
            .andReturn();
    // 取得したレスポンスをログに出力しておくよ
    log.info("external response : {}", mvcResult.getResponse().getContentAsString());
    // verify
    verify(externalService, times(1)).getExternalResource();  // モックの呼び出しは1回を検証
  }
}
```

この変更で特に大事なのは `ExternalService` が外部リソースにアクセスせずモックデータを返すようになったことです。

これで外部リソースに依存せずにControllerのテストが実行できるようになりました。

##3-2. 片方をDI、もう片方をMock化してテストする
先ほどのテストでは `DemoService` と　`ExternalService` の両方をMock化しましたが **片方だけMock化する** こともできます。
例えば `ExternalService` のみをMock化したい場合は以下のようになります。

```java:DemoControllerWithOneSideMockTest.java
package com.example.demo.controller;

import com.example.demo.filter.LogFilter;
import com.example.demo.service.ExternalService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@EnableWebMvc
@Slf4j
class DemoControllerWithOneSideMockTest {

  MockMvc mockMvc;

  @Autowired WebApplicationContext webApplicationContext;
  @Autowired LogFilter logFilter;

  @MockBean ExternalService externalService;    // ExternalServiceは外部アクセスがあるのでMock化する

  @BeforeEach
  void beforeEach() {
    MockitoAnnotations.initMocks(this);
    mockMvc =
        MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .addFilter(logFilter, "/*")
            .build();
  }

  @AfterEach
  void afterEach() {}

  @Test
  void hello() throws Exception {
    mockMvc
        .perform(get("/"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("success"))
        .andExpect(jsonPath("$.message").value("request succeeded."))
        .andExpect(jsonPath("$.data").value("hello"));
  }

  @Test
  void divideSuccess() throws Exception {
    mockMvc
        .perform(get("/divide/10/3"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("success"))
        .andExpect(jsonPath("$.message").value("request succeeded."))
        .andExpect(jsonPath("$.data").value("3.33"));
  }

  @Test
  void divideInvalidParameter() throws Exception {
    mockMvc
        .perform(get("/divide/10/aaa"))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("divide.num2:must match \"[0-9]*\""));
  }

  @Test
  void divideZeroError() throws Exception {
    mockMvc
        .perform(get("/divide/10/0"))
        .andExpect(status().is5xxServerError())
        .andExpect(jsonPath("$.status").value("failure"))
        .andExpect(jsonPath("$.message").value("error has occurred."));
  }

  // 外部リソースの取得だけMockを使って検証するよ
  @Test
  void getExternalResource() throws Exception {
    // mock
    when(externalService.getExternalResource()).thenReturn("this is mock data for internal test.");
    // request
    MvcResult mvcResult =
        mockMvc
            .perform(get("/external"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("success"))
            .andExpect(jsonPath("$.message").value("request succeeded."))
            .andExpect(jsonPath("$.data").value("this is mock data for internal test."))
            .andReturn();
    //
    log.info("external response : {}", mvcResult.getResponse().getContentAsString());
    // verify
    verify(externalService, times(1)).getExternalResource();
  }
}
```
これでテスト時にBeanをDIするも、Mock化するも自由自在です。とても便利！！

#4. パラメータ化テスト
最後に `Service` の単体テストを例にJUnit5で追加された便利機能 `パラメータ化テスト` もサンプルを残しておきます。
テストデータだけを変えて同じケースを繰り返しテストすることが簡単にできるようになります。

```java:DemoServiceTest.java
package com.example.demo.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

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

@SpringBootTest
class DemoServiceTest {

  @Autowired DemoService demoService;

  @Test
  void hello() {
    assertEquals("hello", demoService.hello());
  }

  // @ParameterizedTest で割り算メソッドをいろんな入力パターンでテストするよ
  @ParameterizedTest
  @MethodSource("divideTestArgs")  // "devideTestArgs"という名前のstaticメソッドを引数のソースに使うよ
  void divide(String b1, String b2, String strExpect, boolean hasError) {

    BigDecimal expect = Optional.ofNullable(strExpect).map(BigDecimal::new).orElse(null);
    BigDecimal actual = null;
    Exception error = null;
    // 割り算メソッド実行
    try {
      actual = demoService.divide(new BigDecimal(b1), new BigDecimal(b2));
    } catch (Exception e) {
      error = e;
    }
    // 期待値と検証
    assertEquals(expect, actual);
    // エラーが発生していないか検証
    assertEquals(hasError, error != null);
  }

  // divideテストのパラメータリスト
  static List<Object[]> divideTestArgs() {
    return List.of(
        new Object[] {"1", "1", "1.00", false},
        new Object[] {"0", "1", "0.00", false},
        new Object[] {"5", "2", "2.50", false},
        new Object[] {"10", "3", "3.33", false}, // 四捨五入（小数点第三桁切り捨て）
        new Object[] {"11", "3", "3.67", false}, // 四捨五入（小数点第三桁切り上げ）
        new Object[] {"1", "0", null, true}); // ゼロ除算
  }
}
```
他にも `@ ~ Source` は

- `＠EnumSource` ... Enumの全定数や特定の定数リスト
- `＠MethodSource` ... Methodの戻り値リスト（上記例）
- `＠CsvSource` ...  CSVテキスト
- `@CsvFileSource` ... CSVファイル

などをソースとして利用できるようです。

JUnit５の機能については [こちらのページ](https://qiita.com/opengl-8080/items/efe54204e25f615e322f#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E5%8C%96%E3%83%86%E3%82%B9%E3%83%88) が非常にわかりやすかったです！
とても参考にさせていただきましたmm

# あとがき
JUnitのテストに関する記事はたくさんありますが、古いバージョンの記事もかなり多いので、SpringBootとJUnit5からチャレンジするビギナーの方々の参考になれば嬉しいです。