Mockitoでfinalメソッドをモック化する

# はじめに

こちらで記事にした内容の最新版です.
前回の記事から年月が流れ、Mockitoもバージョン4となりPowermockが不要になったため書き直しました（サンプルコードも書き換え）.

https://qiita.com/takayuki206/items/3f417926195b36b264e5

## 実行環境

EclipseでMavenプロジェクトを作成してコードを作成しました.
「Eclipse IDE for Java Developers」か「Eclipse IDE for Enterprise Java and Web Developers」をインストールすると環境は整います.
ソースコードはJava8で作成していますがJava8以降でも使用できます.

## Maven依存関係

pom.xmlには主にテスト用のアーティファクトを追加しています.
テスト実行はJUnit5を使用しましたがJunit4でも動くと思います.
テストでモックを使用するフレームワークに[Mockito](https://site.mockito.org/)を使用しています(今回のキモ).
また、テストの検証に[Assertj](https://assertj.github.io/doc/)を使用しています.

```xml:pom.xml
	<dependencies>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>5.8.2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-inline</artifactId>
			<version>4.5.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
			<version>4.5.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.assertj</groupId>
			<artifactId>assertj-core</artifactId>
			<version>3.24.2</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
```

## テストするコード

今回は以下のようなコードを作成してテストを行ってみました.

```java:App.java
package example;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class App {

  private final Configuration config;

  public App(Configuration config) {
    this.config = config;
  }

  public void execute(byte[] payload) throws IOException {
    String host = config.getStringProperty("host");
    int port = config.getIntegerProperty("port");
    try (Socket socket = new Socket()) {
      socket.connect(new InetSocketAddress(host, port));
      OutputStream output = socket.getOutputStream();
      output.write(payload);
      output.flush();
    }
  }
}
```

```java:Configuration.java
package example;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class Configuration {

  private Properties props;

  public Configuration(Path config) throws IOException {
    try (InputStream input = Files.newInputStream(config)) {
      props = new Properties();
      props.load(input);
    }
  }

  public final Integer getIntegerProperty(String name) {
    String value = props.getProperty(name);
    return value != null ? Integer.valueOf(value) : null;
  }

  public final String getStringProperty(String name) {
    return props.getProperty(name);
  }
}
```

## テストコード

続いてテストコードです.

### finalメソッドのテスト

最初に、finalメソッド、new演算子でのモック化を含むテストコードです.

```java:AppTest.java
  @Test
  void test_execute_01() {
    OutputStream mockOutputStream = Mockito.mock(OutputStream.class);

    try (MockedConstruction<Socket> mocked =
        Mockito.mockConstruction(Socket.class, (mock, context) -> {
          Mockito.when(mock.getOutputStream()).thenReturn(mockOutputStream);
        }); // 1-1
    ) {
      // Arrange
      Configuration config = Mockito.mock(Configuration.class);
      // 1-2
      Mockito.when(config.getStringProperty("host")).thenReturn("example");
      Mockito.when(config.getIntegerProperty("port")).thenReturn(123);

      App target = new App(config);

      byte[] payload = new byte[0];

      // Act
      target.execute(payload);

      // Assert
      // 1-3
      Assertions.assertThat(mocked.constructed()).hasSize(1);
      Socket mock = mocked.constructed().get(0);

      // 1-4
      ArgumentCaptor<SocketAddress> captor = ArgumentCaptor.forClass(SocketAddress.class);
      Mockito.verify(mock, Mockito.times(1)).connect(captor.capture());
      SocketAddress address = captor.getValue();

      // 1-5
      Assertions.assertThat(address)
          .asInstanceOf(InstanceOfAssertFactories.type(InetSocketAddress.class))
          .extracting(t -> t.getHostName(), t -> t.getPort()).containsSubsequence("example", 123);

      // 1-6
      Mockito.verify(mockOutputStream, Mockito.times(1)).write(ArgumentMatchers.eq(payload));
      Mockito.verify(mockOutputStream, Mockito.times(1)).flush();
    } catch (IOException e) {
      Assertions.fail("test failed.", e);
    }
  }
```

- [1-1] `org.mockito.Mockito.mockConstruction(Class, MockInitializer)`を使用して、new演算子によるインスタンス化でモックを返すようにできます.
1つ目の引数にモック化するクラスを指定します.
2つ目の引数でモックの振る舞いを設定します.

:::note
`Mockito#mockConstruction(Class, MockInitializer)`メソッドで`MockedConstruction`を取得して、`MockedConstruction#close()`を呼び出すまではnew演算子によりモックを生成する.
```java
try (MockedConstruction<T> mocked = Mockito.mockConstruction(T.class, (mock, context) -> { })) {
  // new T() はモックを生成する
}
```
:::

:::note warn
`mockConstruction`メソッドの戻り値である`MockedConstruction`の`close`メソッドを呼び出すまで有効です.
`close`メソッドの呼び出し漏れがあると他のテストに影響を及ぼす可能性があります.
:::

- [1-2] メソッドの振る舞いを設定します. `mockito:mockito-inline`がクラスパスに追加されていれば、finalメソッドも同様に設定できます.

:::note warn
finalメソッドに対する振る舞いは、依存関係に`org.mockito:mockito-inline`が追加されていないとできません.
:::

- [1-3] `MockedConstruction#construction()`メソッドで取得するListからモックを取得できます(この場合`Socket`のモックです).
このListには呼び出された回数の要素が追加されます. この例では`new Socket()`が1度だけ呼ばれているのでListの要素数1です.

- [1-4] `ArgumentCaptor`を使用して、`Socket#connect(SocketAddress)` が呼び出された時の引数を取得しています.

- [1-5] `SocketAddress`が`InetSocketAddress`クラスでホスト名とポート番号が、`InetSocketAddress`のコンストラクタで渡した値であるかを検証します.

- [1-6] `Socket#getOutputStream()`が返却した`OutputStream`への呼び出しを検証します.

### staticメソッドのテスト

次に、上記に加えてstaticメソッドのモック化を含んだテストコードです.

```java:AppTest.java
  @Test
  void test_execute_02() {
    OutputStream mockOutputStream = Mockito.mock(OutputStream.class);

    try (MockedStatic<Files> mockedFiles = Mockito.mockStatic(Files.class); // 2-1
        MockedConstruction<Socket> mockedSocket =
            Mockito.mockConstruction(Socket.class, (mock, context) -> {
              Mockito.when(mock.getOutputStream()).thenReturn(mockOutputStream);
            });) {

      // Arrange
      Path path = Mockito.mock(Path.class);
      byte[] propertiesByteArray = "host=example\nport=123".getBytes();
      InputStream input = new ByteArrayInputStream(propertiesByteArray);
      // 2-2
      mockedFiles.when(() -> Files.newInputStream(path)).thenReturn(input);

      Configuration config = new Configuration(path);
      App target = new App(config);

      byte[] payload = new byte[0];

      // Act
      target.execute(payload);

      // Assert
      // 2-3
      Assertions.assertThat(mockedSocket.constructed()).hasSize(1);
      Socket mock = mockedSocket.constructed().get(0);

      // 2-4
      ArgumentCaptor<SocketAddress> captor = ArgumentCaptor.forClass(SocketAddress.class);
      Mockito.verify(mock, Mockito.times(1)).connect(captor.capture());
      SocketAddress address = captor.getValue();

      // 2-5
      Assertions.assertThat(address)
          .asInstanceOf(InstanceOfAssertFactories.type(InetSocketAddress.class))
          .extracting(t -> t.getHostName(), t -> t.getPort()).containsSubsequence("example", 123);

      // 2-6
      Mockito.verify(mockOutputStream, Mockito.times(1)).write(ArgumentMatchers.eq(payload));
      Mockito.verify(mockOutputStream, Mockito.times(1)).flush();
    } catch (IOException e) {
      Assertions.fail("test failed.", e);
    }
  }
}
```

- [2-1] `org.mockito.Mockito.mockStatic(Class)`を使用して、staticメソッドをモック化することができます.
1つ目の引数にモック化するメソッドを持つクラスを指定します.

:::note
`Mockito#mockStatic(Class)`メソッドで`MockedStatic`を取得して、`MockedStatic#close()`を呼び出すまではstaticメソッドをモック化できる.
```java
try (MockedStatic<T> mocked = Mockito.mockStatic(T.class)) {
  mocked.when(() -> {
    // モック化するstaticメソッドを呼び出すコードを記述する
  }).thenReturn(...);
}
```
:::

:::note warn
`mockStatic`メソッドの戻り値である`MockedStatic`の`close`メソッドを呼び出すまで有効です.
`close`メソッドの呼び出し漏れがあると他のテストに影響を及ぼす可能性があります.
:::

- [2-2] 2-1 で取得した`MockedStatic`を使用してstaticメソッドをモック化できます.
`org.mockito.MockedStatic.when(Verification)` に、モック化するstaticメソッドを実行するコードを渡します.
使い方は`org.mockito.Mockito.when(Object)`と同じです.

- [2-3]～[2-6] は、[1-3][1-6]と同じ内容です.

### テストコード全体

最後にテストコード全体を載せておきます.

<details><summary>テストコード(全体)</summary>

```java:AppTest.java(全体)
package example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.MethodOrderer.MethodName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

@TestMethodOrder(MethodName.class)
class AppTest {

  /**
   * new演算子でモック化するサンプル(MockedConstruction).
   */
  @Test
  void test_execute_01() {
    OutputStream mockOutputStream = Mockito.mock(OutputStream.class);

    try (MockedConstruction<Socket> mocked =
        Mockito.mockConstruction(Socket.class, (mock, context) -> {
          Mockito.when(mock.getOutputStream()).thenReturn(mockOutputStream);
        }); // 1-1
    ) {
      // Arrange
      Configuration config = Mockito.mock(Configuration.class);
      // 1-2
      Mockito.when(config.getStringProperty("host")).thenReturn("example");
      Mockito.when(config.getIntegerProperty("port")).thenReturn(123);

      App target = new App(config);

      byte[] payload = new byte[0];

      // Act
      target.execute(payload);

      // Assert
      // 1-3
      Assertions.assertThat(mocked.constructed()).hasSize(1);
      Socket mock = mocked.constructed().get(0);

      // 1-4
      ArgumentCaptor<SocketAddress> captor = ArgumentCaptor.forClass(SocketAddress.class);
      Mockito.verify(mock, Mockito.times(1)).connect(captor.capture());
      SocketAddress address = captor.getValue();

      // 1-5
      Assertions.assertThat(address)
          .asInstanceOf(InstanceOfAssertFactories.type(InetSocketAddress.class))
          .extracting(t -> t.getHostName(), t -> t.getPort()).containsSubsequence("example", 123);

      // 1-6
      Mockito.verify(mockOutputStream, Mockito.times(1)).write(ArgumentMatchers.eq(payload));
      Mockito.verify(mockOutputStream, Mockito.times(1)).flush();
    } catch (IOException e) {
      Assertions.fail("test failed.", e);
    }
  }

  @Test
  void test_execute_02() {
    OutputStream mockOutputStream = Mockito.mock(OutputStream.class);

    try (MockedStatic<Files> mockedFiles = Mockito.mockStatic(Files.class); // 2-1
        MockedConstruction<Socket> mockedSocket =
            Mockito.mockConstruction(Socket.class, (mock, context) -> {
              Mockito.when(mock.getOutputStream()).thenReturn(mockOutputStream);
            });) {

      // Arrange
      Path path = Mockito.mock(Path.class);
      byte[] propertiesByteArray = "host=example\nport=123".getBytes();
      InputStream input = new ByteArrayInputStream(propertiesByteArray);
      // 2-2
      mockedFiles.when(() -> Files.newInputStream(path)).thenReturn(input);

      Configuration config = new Configuration(path);
      App target = new App(config);

      byte[] payload = new byte[0];

      // Act
      target.execute(payload);

      // Assert
      // 2-3
      Assertions.assertThat(mockedSocket.constructed()).hasSize(1);
      Socket mock = mockedSocket.constructed().get(0);

      // 2-4
      ArgumentCaptor<SocketAddress> captor = ArgumentCaptor.forClass(SocketAddress.class);
      Mockito.verify(mock, Mockito.times(1)).connect(captor.capture());
      SocketAddress address = captor.getValue();

      // 2-5
      Assertions.assertThat(address)
          .asInstanceOf(InstanceOfAssertFactories.type(InetSocketAddress.class))
          .extracting(t -> t.getHostName(), t -> t.getPort()).containsSubsequence("example", 123);

      // 2-6
      Mockito.verify(mockOutputStream, Mockito.times(1)).write(ArgumentMatchers.eq(payload));
      Mockito.verify(mockOutputStream, Mockito.times(1)).flush();
    } catch (IOException e) {
      Assertions.fail("test failed.", e);
    }
  }
}
```
</details>