LoginSignup
3
3

More than 1 year has passed since last update.

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

Posted at

はじめに

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

実行環境

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

Maven依存関係

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

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>

テストするコード

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

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();
    }
  }
}
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演算子でのモック化を含むテストコードです.

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つ目の引数でモックの振る舞いを設定します.

Mockito#mockConstruction(Class, MockInitializer)メソッドでMockedConstructionを取得して、MockedConstruction#close()を呼び出すまではnew演算子によりモックを生成する.

try (MockedConstruction<T> mocked = Mockito.mockConstruction(T.class, (mock, context) -> { })) {
  // new T() はモックを生成する
}

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

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

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] SocketAddressInetSocketAddressクラスでホスト名とポート番号が、InetSocketAddressのコンストラクタで渡した値であるかを検証します.

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

staticメソッドのテスト

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

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つ目の引数にモック化するメソッドを持つクラスを指定します.

Mockito#mockStatic(Class)メソッドでMockedStaticを取得して、MockedStatic#close()を呼び出すまではstaticメソッドをモック化できる.

try (MockedStatic<T> mocked = Mockito.mockStatic(T.class)) {
  mocked.when(() -> {
    // モック化するstaticメソッドを呼び出すコードを記述する
  }).thenReturn(...);
}

mockStaticメソッドの戻り値であるMockedStaticcloseメソッドを呼び出すまで有効です.
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]と同じ内容です.

テストコード全体

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

テストコード(全体)
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);
    }
  }
}
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3