Assertions.assertThrows() の引数　Executableって何？

# はじめに
JavaとSpringBootフレームワークを使用したWebアプリケーション開発の勉強をしています。

JUnit5で単体テストのテストメソッドを実装する上で、例外のスローを検証したいと考えた際に戸惑った部分がありました。
```java
package org.junit.jupiter.api;

public class Assertions {
    /**省略*/

    public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) {
        return (T)AssertThrows.assertThrows(expectedType, executable);
      }
}
```
例外が投げられているかの検証として`assertThrows`を使用したかったのですが、「引数の`Class<T> expectedType`はわかるとして`Executable executable`は何…？」
と困惑してしまったので、それについて調べたことを初学者の視点から備忘録として残したいと思います。
伝わりやすさに重点を置いて書いているので、しばしば表現が厳密でない部分もあるかとおもいますが、ご容赦ください。

# 成功したテストメソッドの実装
先んじて、どのように書いてコンパイルが通ったかを記しておきたいと思います。
<details>
<summary>長いので周辺の詳細は適宜ご参照ください</summary>

**例として用いる処理**
`id`から製品情報(`Product`)を検索します。  
データベースに`id`と一致するレコードがない場合に、サービス層でカスタム例外クラス`InvalidIdException`をスローします。下記のシーケンス図の色付きの部分をテスト対象とします。  
```mermaid  
sequenceDiagram  
    Actor User  
    participant C As ProductController  
    participant S As ProductService  
    participant R As ProductRepository  
    Note over User: IDで製品情報を検索したい  
    User->>C: idを渡す  
    C->>S: searchProduct(id)を呼び出す  
    S->>R: selectProduct(id)を呼び出す  
    R-->>S: Productを返す  
    rect rgb(191, 223, 255)  
    S->>S: Productの中身を確かめる  
    break [Productがnull]  
        S-->>C: InvalidIdExceptionを投げる  
    end  
    end  
    S-->>C: Productを返す  
    C-->>User: Productを返す  
    
```

```java  
public class ProductService {

    private ProductRepository repository; 

    @Autowired
    public StudentService(ProductRepository repository) {
    this.repository = repository;
    }

    // ↓テスト対象メソッド
    public Product searchProduct(int id) {
        Product product = repository.selectProduct(id);
        if (product == null) {
            throw new InvalidIdException(id);
        } else {
            return product;
        }
    }
}

```
```java
public class ProductRepository {
    public Product selectProduct(int id){
        // DBでidに一致するレコードを取得し、Productインスタンスとして呼び出し元にリストで返す
        // 一致するレコードがない場合は呼び出し元にnullを返す
    }
}
```

```java
public class InvalidIdException extends RuntimeException {
    public InvalidIdException(int id) {
        super("存在しないIDです：" + id);
    }
}
```
</details>

### テスト対象メソッド
```java
public class ProductService {

    private ProductRepository repository; 

    @Autowired
    public ProductService(ProductRepository repository) {
    this.repository = repository;
    }

    // ↓テスト対象メソッド
    public Product searchProduct(int id) {
        Product product = repository.selectProduct(id);
        if (product == null) {
            throw new InvalidIdException(id);
        } else {
            return product;
        }
    }
}

```
### テストメソッド
```java
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock
    private ProductRepository repository;
    
    private ProductService sut;
    
    @BeforeEach
    private void before() {
    sut = new ProductService(repository);
    }

    @Test
    void 製品検索失敗_リポジトリの処理を呼び出し戻り値がnullの場合は例外を投げていること()
          throws Exception {
        // Arrange
        int id = 99;
        Product nullProduct = null;
        Mockito.when(repository.selectProduct(id)).thenReturn(nullProduct);;
    
        // Act & Assert
        Assertions.assertThrows(
            InvalidIdException.class, () -> {
              sut.searchProduct(id);
            });
        Mockito.verify(repository, times(1)).selectProduct(id);
    }
}
```
第２引数`Executable executable`として、`() -> {
              sut.searchProduct(id);}`を渡しています。
Perplexityに手伝ってもらいながら書きました。

Perplexityが言うには、

**『Executable executableには、「例外が発生することを期待する処理」をラムダ式またはメソッド参照で渡します。
このExecutableは関数型インターフェースで、引数なし・戻り値なしのvoidメソッド（例：void run() throws Throwable）を実装するものです』**

とのことです。

とりあえず、**例外が発生することを検証したいテスト対象メソッドが、引数が１つだけ**なのであれば、上記のコードブロック内の`sut.searchProduct(id);`を丸々任意のテスト対象メソッドに書き換えればコンパイルは通りそうです。

# Executableについてもう少し詳しく
Executableをはじめとした**関数型インタフェース**は、共通して「ラムダ式またはメソッド参照で実装できる」そうなので、関数型インタフェースを知っていればすぐ答えが出たのだと思うのですが、私は知らなかったのでPerplexityが何を言っているのか全く理解できませんでした。
そこで、もう少し掘り下げていきたいと思います。

## 関数型インタフェースとラムダ式
ざっくり説明すると、関数型インタフェースは **「関数(処理)そのものを引数として受け取ったり、返り値として返したりすることをサポートするもの」** のことです。
また、成立条件として「抽象メソッドを一つだけ持っているインタフェースである」必要があります。
たとえばExcutableインタフェースの中身はこれだけです↓
```java
package org.junit.jupiter.api.function;
/**省略 */

@FunctionalInterface
@API( /**省略 */)
public interface Executable {
  void execute() throws Throwable;
}
```

Javaにおけるメソッドは、基本的に受け取ったり返したりするのはオブジェクト(プリミティブ型の値含む)なので、ピンと来ないと思います。
関数型インタフェースでは、**関数(処理)をラムダ式としてオブジェクト(インスタンス)化する**ことで、関数を受け取ったり返したりということを可能にしています。

:::note info
**ラムダ式**
私はラムダ式のことを「コードを簡略化できるもの」という程度にしか認識していなかったので苦戦していました。そもそもラムダ式は、「関数型インタフェースの実装を簡略化するために用意されたもの」で、ラムダ式を書くところは常に関数型インタフェースを実装しているんだそうです。
:::

### 関数(処理)をインスタンス化するとは
```java
        Assertions.assertThrows(
            InvalidIdException.class, () -> {
              sut.searchProduct(id);
            });
```
前述したassertThrows()メソッドは、ラムダ式を第2引数として渡しています。
「ラムダ式がインスタンスになるってどういうこと？:disappointed_relieved:」
と混乱してしまうのですが、ラムダ式を使わずに同等の動きをするコードを書こうとすると以下のようになります。
```java
Assertions.assertThrows(
    InvalidIdException.class,
    // ラムダ式を無名クラスに置き換えて記述
    new Executable() {
        @Override
        public void execute() throws Throwable {
            sut.searchProduct(id);
        }
    }
);
```
(無名クラスについては、ここで暗黙的に無名のクラスを宣言しているとだけ認識していてください)

上記のコードでは、第2引数でExecutableのインスタンスを生成しています。
そして、Executableはインタフェースで抽象メソッドしか持たないため、インスタンスを生成したときに必ずオーバーライドして処理の中身を実装する必要があります。
つまり、Executable型のインスタンス（実体はExecutableをimplementした無名クラスのインスタンス）に、プログラマが定義した処理が組み込まれています。
ラムダ式は、無名クラスを使ったこのようなコードを簡略化し、プログラマが実際に意味のある処理以外をほぼ書かなくていいようにしています。
このような仕組みによって、**見かけ上「処理(関数)をインスタンスとして渡す」ことが実現可能となる**のです。

`Assertions.assertThrows()`では、引数で受け取ったこの処理を自身のメソッド内の`Executable.execute()`で実行し、try/catchで例外の発生を検証しています。

# おわりに
・Executableは、関数型インタフェースである
・`Executable executable`には、Executableインタフェースを実装した無名クラスのインスタンスを渡す。
・関数型インタフェースを実装した無名クラスのインスタンスは、ラムダ式で表現できる。
上記がまとめとなります。

Executableって何だろう、関数型インタフェースって何だろうという疑問を端に発した調べものでしたが、ラムダ式に対する勘違いを正すことができたり、とても有用でした。

# 参考

https://qiita.com/KenyaSaitoh/items/98be8b9140aa195409d7
https://zenn.dev/jk447/articles/0a1a5d5f053f02
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/function/package-summary.html