はじめに
知識のおすそ分けです。
目次
開発環境
- JUnit 5
- Java 8 以降
単体テスト対象のメソッド in 抽象クラス
以下のような抽象クラス AbstractMyClass.java
に定義された getHogeString()
メソッドに単体テストをかぶせたい!という状況を想定します。
public abstract class AbstractMyClass {
private static final String HOGE_STRING = "hoge";
// テスト対象メソッド
public String getHogeString() {
return HOGE_STRING;
}
// 抽象メソッド(テスト対象外とする)
protected abstract void doSomething();
}
抽象クラスに単体テストを書くときのハードル
< 抽象クラスはインスタンス化できない...。
public class AbstractMyClassTest {
@Test
void test() {
// エラーが起きる:Cannot instantiate the type AbstractMyClass
AbstractMyClass abstractMyClass = new AbstractMyClass();
}
}
抽象クラスに単体テストをあてる方法5つを紹介します
- 方法1.抽象クラスが継承されている具象クラスに単体テストを書く
- 方法2.抽象クラスを継承する具象クラスを新たに作成して単体テストを書く
- 方法3.テストクラスで匿名クラスを作成して単体テストを書く
- 方法4.
Mockito.Spy
を使う - 方法5.
Mockito.CALLS_REAL_METHODS
を使う
方法1.抽象クラスが継承されている具象クラスに単体テストを書く
まず、テスト対象の抽象クラスが継承されている具象クラスを見つけます。例えば以下のようなクラスを見つけたとします。
public class MyClass extends AbstractMyClass {
@Override
protected void doSomething() {
// このクラス固有の処理
}
public String fugaString() {
return "huga";
}
public String piyoString() {
return "piyo";
}
}
テスト対象の抽象クラスを継承している具象クラスのインスタンスを使って、抽象クラスが持つメソッドの単体テストを書くことができます。
public class MyClassTest {
@Test
@DisplayName("すでにある具象クラスを利用して、抽象クラスのメソッドに対するテストを書く")
void test() {
MyClass it = new MyClass();
String actual = it.getHogeString();
assertThat(actual, is("hoge"));
}
}
注意点:
抽象クラスを継承した具象クラスが複数存在する場合、抽象クラスが持つメソッドの単体テストがバラけるので、類似の無駄な単体テストが増える可能性がある。
方法2.抽象クラスを継承する具象クラスを新たに作成して単体テストを書く
テスト対象クラスを継承させたテスト用の具象クラスを新たに作成します。
public class ConcreteMyClass extends AbstractMyClass {
@Override
protected void doSomething() {
// このメソッドはテスト対象外なのでなにも定義しない
}
}
新規作成したテスト用の具象クラスをインスタンス化して、単体テストを書くことができます。
public class ConcreteMyClassTest {
@Test
@DisplayName("抽象クラスのテスト用に具象クラスを作成し、そのクラスのテストを書く")
void test() {
ConcreteMyClass it = new ConcreteMyClass();
String actual = it.getHogeString();
assertThat(actual, is("hoge"));
}
}
抽象クラスを継承した複数クラスにテストが散らばるリスクがないので、この方法が一番おすすめです。
方法3.テストクラスで匿名クラスを作成して単体テストを書く
テストクラスで匿名クラスを利用することで、一時的に抽象クラスを継承したクラスをインスタンス化することができます。
匿名クラスは直接インスタンス化される無名のクラスであり、クラスの宣言とインスタンスの生成を同時に行うことができます。匿名クラスは 抽象クラス・インターフェース・具象クラスを拡張または実装することができるので、こいつをテストクラスでうまいこと使います。
public abstract class AbstractMyClass {
private static final String HOGE_STRING = "hoge";
public String getHogeString() {
return HOGE_STRING;
}
// 抽象メソッドがない場合は新たに追加する
protected abstract void initialize();
}
テストクラスで抽象クラス AbstractMyClass.java
を継承した匿名クラスを定義し、initialize()
メソッドをオーバーライドしています。その後、匿名クラスのインスタンスを作成し、getHogeString()
メソッドを呼び出しています。
public class AbstractMyClassTest {
@Test
@DisplayName("匿名クラスを利用して一時的にインスタンス化する")
void test() {
AbstractMyClass it = new AbstractMyClass() {
@Override
protected void initialize() {
// なにもしない
};
};
String actual = it.getHogeString();
assertThat(actual, is("hoge"));
}
}
注意点:
匿名クラスは再利用できないので、単体テストで使用する場合はテストメソッドごとに毎回新しい匿名クラスのインスタンスを作成する必要があります。いちおう、@BeforeEach
アノテーションなどを使った setUp()
メソッド内でインスタンス化をすればコードの重複は避けることができます。
方法4.Mockito.Spy
を使う
@Spy
の利用は、テスト対象の抽象クラスが具体的な実装を持つメソッドを含む場合にけっこう便利な方法です。@Spy
の利用で抽象クラスの偽装オブジェクトを作成し、抽象クラスに定義されたメソッドを呼び出すことができます。
public class AbstractMyClassTest {
private AutoCloseable closeable;
@Spy
AbstractMyClass abstractMyClass;
@BeforeEach
void setUp() {
closeable = MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() throws Exception {
closeable.close();
}
@Test
@DisplayName("Mockito.Spyを使って呼びだす")
void getHogeStringTest() {
assertThat(abstractMyClass.getHogeString(), is("hoge"));
}
}
補足:
テスト対象の抽象クラスが具体的な実装を持つメソッドを含む場合、かつ、抽象クラスに実装が定義されたメソッドの単体テストでは、@Spy
の方が @Mock
よりも有効であると言えます。@Mock
と @Spy
の特性を比較すると分かりやすいです。
-
@Mock
の特性:-
@Mock
を使用すると、クラスの新しい偽装オブジェクト(モック)が作成される - このモックはそのクラスのすべてのメソッドをオーバーライドし、デフォルトの動作(例えばnullを返す、空のリストを返すなど)を提供する
- モックのメソッドはテスト内で特定の動作(
when().thenResturn()
やdoRetuen().when()
など)を設定することができる
-
-
@Spy
の特性:-
@Spy
を使用するとクラスの偽装オブジェクト(スパイオブジェクト)が作成され、そのインスタンスのメソッド呼び出しを監視する - スパイオブジェクトはそのクラスの一部をモック化せず、元の実装を保持する
- また、モックと同様にテスト内で特定の動作をするように設定することも可能
-
抽象クラスには、具体的な実装が定義されたメソッドとサブクラスによって実装される抽象メソッドがあります。具体的な実装が定義されたメソッドの単体テストを行う場合、@Spy
を使用すると、そのメソッドの実際の振る舞いを呼び出すことができます。
一方、@Mock
を使用するとそのメソッドの振る舞いはデフォルトの動作に置き換えられる、あるいはテスト内で設定した動作になります。そのため、@Mock
を使用した場合の単体テストはテストクラスで設定した振る舞いのテストとなり、そのメソッドの実際の振る舞いを検証することはできません。
以上の理由から、抽象クラスに実装が定義されたメソッドの単体テストでは、@Spy
の方が @Mock
よりも有効であると言えます。
抽象クラスに実装が定義されたメソッドの単体テストでは @Mock
ではなく @Spy
を使おう。
方法5.Mockito.CALLS_REAL_METHODS
を使う
CALLS_REAL_METHODS
はモックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すように設定できるやつです。
public class AbstractMyClassTest {
@Test
@DisplayName("Mockito.CALLS_REAL_METHODSを使って呼びだす")
void test() {
AbstractMyClass it = mock(AbstractMyClass.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
String actual = it.getHogeString();
assertThat(actual, is("hoge"));
}
}
CALLS_REAL_METHODS
の設定により、getHogeString()
が抽象クラス内で実装されているので実装がそのまま呼び出されます。これにより抽象クラスの getHogeString()
メソッドの振る舞いを確認する単体テストを行うことができます。
注意点:
CALLS_REAL_METHODS
を使用すると、モックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すようになり、予期しないエラーが出たりするのであんまりオススメしません。
おわりに
他にも方法あるかな?