はじめに
こちらで記事にした内容の最新版です.
前回の記事から年月が流れ、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を使用しています.
<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>
テストするコード
今回は以下のようなコードを作成してテストを行ってみました.
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();
}
}
}
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演算子でのモック化を含むテストコードです.
@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
メソッドの戻り値であるMockedConstruction
のclose
メソッドを呼び出すまで有効です.
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]
SocketAddress
がInetSocketAddress
クラスでホスト名とポート番号が、InetSocketAddress
のコンストラクタで渡した値であるかを検証します. -
[1-6]
Socket#getOutputStream()
が返却したOutputStream
への呼び出しを検証します.
staticメソッドのテスト
次に、上記に加えてstaticメソッドのモック化を含んだテストコードです.
@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
メソッドの戻り値である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]と同じ内容です.
テストコード全体
最後にテストコード全体を載せておきます.
テストコード(全体)
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);
}
}
}