はじめに
本記事では、テスタビリティを高めるためのリファクタリングやモック/スタブによるテスト、Mockitoを用いたテストについて説明していきます。
JUnitの基礎や基本機能の活用方法については前回までの記事で説明していますので、そちらをご参考ください。
JUnitの基本① 基礎知識~簡単なテストメソッドの作成
JUnitの基本② 基本機能の活用
対象者
- Javaの基礎知識を持っている方
- JUnitに初めて触れる方、もしくは少し触れたことのある方
動作環境
- Eclipse 2022(Pleiades All in One)
事前準備
過去の記事を参照ください。
JUnitの基本① 基礎知識~簡単なテストメソッドの作成-事前準備
テスタビリティを高めるリファクタリング
プログラムの外部的な振る舞いを変えず、内部構造を変更してソースコードを整理するというのがリファクタリングですが、テストにおいてもテストしづらいメソッドをテストしやすい形にすることがあります。
例えば、以下のようなシステム時間を取得する処理のテストにおいて、実行時間に差ができる場合は比較すると不一致となってしまいます。
public class DateExample {
private LocalDateTime date;
private String message;
// getter、setterは省略
public void setMessage() {
this.date = LocalDateTime.now();
this.message = "現在時刻:" + date;
}
}
class DateExampleTest {
@Test
void testSetMessage() {
DateExample sut = new DateExample();
sut.setMessage();
assertEquals("現在時刻:" + LocalDateTime.now(), sut.getMessage());
}
}
LocalDateTimeの精度がナノ秒単位のため、実行時間に差ができた場合は失敗します。
このような場合、テストをしやすい形にするため、テスト対象クラスのリファクタリングを行います。
具体的には、LocalDateTime.now()をメソッドとして抽出し、テストクラスからも利用できるようにします。
LocalDateTime.now()を選択した状態でCtrl+1を押下するとクイック・フィックスが表示されるため、「メソッドに抽出します」を押下して、LocalDateTime.now()を別のメソッドに抽出します。
抽出すると以下のような形になりました。
public void setMessage() {
this.date = extracted();
this.message = "現在時刻:" + date;
}
private LocalDateTime extracted() {
return LocalDateTime.now();
}
メソッドの名前とアクセス修飾子を修正しておきます。
※分かりやすい名前とテストクラスからオーバーライドできるようにするため
public void setMessage() {
this.date = newDate();
this.message = "現在時刻:" + date;
}
public LocalDateTime newDate() {
return LocalDateTime.now();
}
次にテストクラスです。変更点は以下になります。
- 現在時刻を格納する定数を定義
- 匿名クラスを定義し、その中でnewDate()メソッドをオーバーライド(予測できるものを返すようにする)
- assertEqualsの期待値をLocalDateTime.now()から定数:currentに変更
class DateExampleTest {
@Test
void testSetMessage() {
final LocalDateTime current = LocalDateTime.now();
DateExample sut = new DateExample() {
@Override
public LocalDateTime newDate() {
return current;
}
};
sut.setMessage();
assertEquals("現在時刻:" + current, sut.getMessage());
}
}
テストがしづらい機能を設計する際には、テスト対象となる部分だけを差し替え可能な設計にしておくと、テストを行いたい機能のテストがしやすくなります。
モック/スタブ
まず、テストダブルについて、テスト対象クラスが依存するオブジェクトを置き換えた代役のことを指し、モックやスタブなどが挙げられます。
テストダブルを使用することで、テスト対象のコードが外部システムに依存せず、独立してテストを実施できるようになります。
スタブ
スタブは事前に定義された振舞いを提供するオブジェクトです。
スタブを使用する場面としては、以下が挙げられます。
- 依存オブジェクトが予測できない振舞いをする場合
例)ランダムな値を扱うときなど - 依存オブジェクトのクラスがまだ存在しない場合
- 依存オブジェクトの実行コストが高く、簡単に利用できない場合
例)大量のデータを扱うときなど - 依存オブジェクトが実行環境に強く依存している場合
このような場合に固定値を返すスタブを設定することで、テストで本来確認したい内容を確認しやすくすることができます。
スタブを使用する場合、テストクラスに直接記述するか、別途スタブクラスを作成するかのどちらかになりますが、それぞれ例を交えて説明していきます。
以下の例はメール送信を行うクラスになりますが、このクラスは実際にメールを送信する外部のクラス(インターフェース)に依存しています。
EmailSenderのテストをするにあたり、MailClientが未完成であったり外部に依存する場合、スタブを使用して疑似的な動作を提供します。
テスト対象クラス
public class EmailSender {
private MailClient mailClient;
public EmailSender(MailClient mailClient) {
this.mailClient = mailClient;
}
public boolean sendEmail(String emailAddress, String message) {
return mailClient.send(emailAddress, message);
}
}
依存するクラス(インターフェース)
public interface MailClient {
boolean send(String emailAddress, String message);
}
テストクラスに直接記述する場合
以下のようにテストクラス内に匿名クラスを定義し、その中でsend()メソッドをオーバーライドして、予測できるもの(true)を返却するようにします。
メリット
- 直接記述するため即座にスタブを用意でき、コードが短く済む
- スタブの挙動をそのテスト専用に定義可能
デメリット
- 他のテストケースで同じスタブが必要になったとしても再度記述が必要なため、再利用性が低い
- スタブが複雑になると、テストクラス内に記述するのが煩雑になる
テストクラス
class EmailSenderTest {
@Test
void testSendEmail() {
// テストクラス内でスタブを直接定義
MailClient stubMailClient = new MailClient() {
@Override
public boolean send(String emailAddress, String message) {
// 送信結果を固定値で返却(常に成功)
return true;
}
};
// テスト対象に依存するスタブを注入
EmailSender emailSender = new EmailSender(stubMailClient);
// テスト実行(メール送信処理が成功したか確認)
boolean result = emailSender.sendEmail("test@example.com", "Hello!");
assertTrue(result); // 常にtrueが返却されることを確認
}
}
別途スタブクラスを作成する場合
今回はMailClientインターフェースを実装するクラスとして、スタブ専用のクラスをsrcの同じパッケージの下に作成します。
中身としては直接記述する場合と同様で、send()メソッドをオーバーライドして、予測できるもの(true)を返却するようにします。
メリット
- 一度スタブクラスを作成すれば複数のテストに使用でき、他のテストケースの容易に追加できるため、再利用性が高い
- スタブのコードをテストケースから分離でき、テストケース自体が簡潔になる
デメリット
- 専用のクラスを作る必要があるため、テストが少ないプロジェクトでは準備に手間がかかる
スタブクラス
public class MailClientStub implements MailClient {
@Override
public boolean send(String emailAddress, String message) {
// 送信結果を固定値で返却(常に成功)
return true;
}
}
テストクラス
class EmailSenderTest {
@Test
void testSendEmail() {
// スタブクラスを使用
MailClientStub mailClientStub = new MailClientStub();
// テスト対象に依存するスタブを注入
EmailSender emailSender = new EmailSender(mailClientStub);
// テスト実行(メール送信処理が成功したか確認)
boolean result = emailSender.sendEmail("test@example.com", "Hello!");
assertTrue(result); // 常にtrueが返却されることを確認
}
}
モック
モックはメソッドの実行に対して実行回数やパラメータの呼び出しを記録するオブジェクトです。
モックを使用する場面としては、対象の依存クラスやモジュールが正しく利用されているかを検証する場合に使用します。
後述するMockitoを使用すれば簡単に設定することができますが、ここでは手動でモッククラスを作成してみます。
モッククラス
public class MailClientMock implements MailClient {
private String emailAddressCalled; // 呼び出されたメールアドレス
private String messageCalled; // 呼び出されたメッセージ
private int callCount = 0; // 呼び出し回数
@Override
public boolean send(String emailAddress, String message) {
this.emailAddressCalled = emailAddress; // 引数を記録
this.messageCalled = message; // 引数を記録
this.callCount++; // 呼び出し回数を記録
return true; // メール送信成功を返却
}
// 呼び出されたメールアドレスを取得
public String getEmailAddressCalled() {
return emailAddressCalled;
}
// 呼び出されたメッセージを取得
public String getMessageCalled() {
return messageCalled;
}
// 呼び出し回数を取得
public int getCallCount() {
return callCount;
}
}
テストクラス
class EmailSenderTest {
@Test
void testSendEmail() {
// モッククラスを使用
MailClientMock mailClientMock = new MailClientMock();
// テスト対象に依存するモックを注入
EmailSender emailSender = new EmailSender(mailClientMock);
// テスト実行(メール送信処理が成功したか確認)
boolean result = emailSender.sendEmail("test@example.com", "Hello!");
assertTrue(result); // 常にtrueが返却されることを確認
assertEquals("test@example.com", mailClientMock.getEmailAddressCalled()); // 呼び出されたメールアドレスを検証
assertEquals("Hello!", mailClientMock.getMessageCalled()); // 呼び出されたメッセージを検証
assertEquals(1, mailClientMock.getCallCount()); // 呼び出し回数を検証
}
}
呼び出し時にオーバーライドしたsend()メソッド内で引数のメールアドレス、メッセージおよび回数を記録し、assertEqualsでそれらが正しく利用されているかを確認しています。
上記をテスト実行すると、正常に終了することが確認できました。
Mockito
Mockitoはモックやスタブに関わる様々な機能を提供するライブラリで、効率的なテストコードの作成が可能となります。
システムの規模が大きくなると用意に時間がかかるため、効率的ではなく準備も大変ですが、Mockitoを使用すれば、アノテーションや簡単なメソッドの記述でモックの設定が可能です。
実装
まず、前準備としてGradleの設定ファイルであるbuild.gradleに依存関係を追加します。
dependencies {
// 中略
testImplementation 'org.mockito:mockito-core:5.5.0' // 最新バージョンを使用
testImplementation 'org.mockito:mockito-inline:5.2.0' // 最新バージョンを使用
testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0' // MockitoのJUnit 5用サポート
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' // JUnit 5
}
では、上で紹介したテスト対象クラスを例に、Mockitoを活用してテストクラスを記述します。
テストクラス
@ExtendWith(MockitoExtension.class) // Mockitoの初期化、終了処理を自動実行
class EmailSenderTest {
@Mock
private MailClient mailClient; // MailClientのモックを自動生成
@InjectMocks
private EmailSender emailSender; // モックをテスト対象クラスに注入
@Test
void testSendEmail() {
String emailAddress = "test@example.com";
String message = "Hello!";
when(mailClient.send(emailAddress, message)).thenReturn(true); // モックの動作を定義
// テスト実行(メール送信処理が成功したか確認)
boolean result = emailSender.sendEmail(emailAddress, message);
// 結果を検証
assertTrue(result); // 常にtrueが返却されることを確認
verify(mailClient).send(emailAddress, message); // 指定した引数でsend()メソッドが呼び出されたか確認
}
}
Mockitoを活用すると、上記の記述だけでモックオブジェクトの生成と、スタブ(振る舞いの定義および設定)の双方を簡単に実現できます。さらに、モックオブジェクトを使用してメソッドの呼び出しを検証することも可能です。
アノテーション等についての説明は以下になります。
- @ExtendWith
クラスに付与することで、Mockitoの拡張機能を使用することができます。(Mockitoの初期化、終了処理を自動実行) - @Mock
付与することでモックオブジェクトを簡単に生成できます。(対象のクラスをモックとして扱える) - @InjectMocks
付与したクラスにモックオブジェクトを注入します。よって、明示的にnewしてインスタンス化する必要がありません。(テスト対象クラスに注入) - when(...).thenReturn(...)
モックの動作を定義して、期待する戻り値を与えます。(スタブの振舞いを定義) - verify(...)
指定した引数でメソッド呼び出しが行われたか検証します。(モックの機能に該当)
補足
スタブの振舞いについて、上記ではthen系のthenReturnを使用していますが、他にも以下のようなメソッドがあります。
then系
主に戻り値を指定するために使用されるスタブ操作で、「事前に何を実行したらどう振舞うか」ということを設定します。
- thenReturn(value):指定した戻り値を返却
- thenThrow(exception):例外をスロー
- thenAnswer(answer):柔軟に動作を設定
- thenCallRealMethod():モックではなく元のメソッドを実行
do系
主に戻り値のないメソッドや例外スローをテストする時に使用されるスタブ操作で、「何をしたらどう振舞うか」ということを設定します。
※後述するSpyモックオブジェクトの場合はdo系メソッドでのみスタブ操作が可能
- doReturn(value):指定した戻り値を返却
- doNothing():何もしない
- doThrow(exception):例外をスロー
- doAnswer(answer):柔軟に動作を設定
- doCallRealMethod():モックではなく元のメソッドを実行
また、モックに渡す引数について、Mockitoでは引数マッチャーというものが提供されており、引数の部分にany()などの引数を渡すことで、任意の値としてマッチするように設定できます。
- any():全ての型の任意の引数にマッチするため、引数を固定せずに柔軟にテストケースを記述したいときに役立つ
- anyString():String型に限定した任意の引数
- anyInt():int型に限定した任意の引数
- anyList():List型に限定した任意の引数
上記のテストクラスの場合、以下に置き換えてもテストを実行できます。
// 実行可能
when(mailClient.send(any(), any())).thenReturn(true); // モックの動作を定義
但し、複数の引数を持つメソッドのテストをする場合、全ての引数に引数マッチャーを使用する必要があります。
// 実行不可
when(mailClient.send(any(), message)).thenReturn(true); // モックの動作を定義
Spy
モックオブジェクトの一種であり、元のクラスの一部の振舞いを保持しつつ、特定の振舞いを変更できる特殊なモックです。
モックは完全に振舞いを模倣するため、実メソッドを呼び出すことはないですが、スパイでは一部のメソッドをモック化し、他のメソッドを元の実装のまま使うことができます。
これまではモック化したいオブジェクトに@Mockを付与していましたが、スパイの場合は@Spyを付与します。
以下のGreetクラスを例に見ていきましょう。
テスト対象クラス
public class Greet {
public String greeting(String name) {
return getGreetingPrefix() + ", " + name + "!";
}
public String getGreetingPrefix() {
return "Hello";
}
}
@Spyを付与してGreetクラスのインスタンスをスパイ化し、doReturn("Hi")~~で特定のメソッド(getGreetingPrefix)のみ挙動を変更しています。
テストクラス
@ExtendWith(MockitoExtension.class)
class GreetTest {
@Spy
private Greet greetSpy; // Greetクラスのインスタンスをスパイ化
@Test
void testGreeting() {
doReturn("Hi").when(greetSpy).getGreetingPrefix(); // 特定のメソッドのみ挙動を変更
String greeting = greetSpy.greeting("John"); //メソッド呼び出し
// 結果を検証
assertEquals("Hi, John!", greeting); // 挙動を変更したため、"Hi" を使用
verify(greetSpy).getGreetingPrefix(); // getGreetingPrefix() が呼ばれたことを確認
verify(greetSpy).greeting("John"); // greeting() メソッドも呼び出しが確認可能
}
}