LoginSignup
8
8

抽象クラスに単体テストをあてる方法5つ紹介します

Posted at

はじめに

知識のおすそ分けです。

目次

開発環境

  • JUnit 5
  • Java 8 以降

単体テスト対象のメソッド in 抽象クラス

以下のような抽象クラス AbstractMyClass.java に定義された getHogeString() メソッドに単体テストをかぶせたい!という状況を想定します。

AbstractMyClass.java
public abstract class AbstractMyClass {

	private static final String HOGE_STRING = "hoge";

    // テスト対象メソッド
	public String getHogeString() {
		return HOGE_STRING;
	}

    // 抽象メソッド(テスト対象外とする)
	protected abstract void doSomething(); 
}

抽象クラスに単体テストを書くときのハードル

:disappointed_relieved: < 抽象クラスはインスタンス化できない...。

AbstractMyClassTest.java
public class AbstractMyClassTest {

	@Test
	void test() {
        // エラーが起きる:Cannot instantiate the type AbstractMyClass
		AbstractMyClass abstractMyClass = new AbstractMyClass();
	}
}

抽象クラスに単体テストをあてる方法5つを紹介します

方法1.抽象クラスが継承されている具象クラスに単体テストを書く

まず、テスト対象の抽象クラスが継承されている具象クラスを見つけます。例えば以下のようなクラスを見つけたとします。

MyClass.java
public class MyClass extends AbstractMyClass {

	@Override
	protected void doSomething() {
		// このクラス固有の処理
	}

	public String fugaString() {
		return "huga";
	}

	public String piyoString() {
		return "piyo";
	}
}

テスト対象の抽象クラスを継承している具象クラスのインスタンスを使って、抽象クラスが持つメソッドの単体テストを書くことができます。

MyClassTest.java
public class MyClassTest {

	@Test
	@DisplayName("すでにある具象クラスを利用して、抽象クラスのメソッドに対するテストを書く")
	void test() {
		MyClass it = new MyClass();
		String actual = it.getHogeString();
		assertThat(actual, is("hoge"));
	}
}

注意点:
抽象クラスを継承した具象クラスが複数存在する場合、抽象クラスが持つメソッドの単体テストがバラけるので、類似の無駄な単体テストが増える可能性がある。

方法2.抽象クラスを継承する具象クラスを新たに作成して単体テストを書く

テスト対象クラスを継承させたテスト用の具象クラスを新たに作成します。

ConcreteMyClass.java
public class ConcreteMyClass extends AbstractMyClass {

	@Override
	protected void doSomething() {
		// このメソッドはテスト対象外なのでなにも定義しない
	}

}

新規作成したテスト用の具象クラスをインスタンス化して、単体テストを書くことができます。

ConcreteMyClassTest.java
public class ConcreteMyClassTest {

	@Test
	@DisplayName("抽象クラスのテスト用に具象クラスを作成し、そのクラスのテストを書く")
	void test() {
		ConcreteMyClass it = new ConcreteMyClass();
		String actual = it.getHogeString();
		assertThat(actual, is("hoge"));
	}

}

抽象クラスを継承した複数クラスにテストが散らばるリスクがないので、この方法が一番おすすめです。

方法3.テストクラスで匿名クラスを作成して単体テストを書く

テストクラスで匿名クラスを利用することで、一時的に抽象クラスを継承したクラスをインスタンス化することができます。

匿名クラスは直接インスタンス化される無名のクラスであり、クラスの宣言とインスタンスの生成を同時に行うことができます。匿名クラスは 抽象クラス・インターフェース・具象クラスを拡張または実装することができるので、こいつをテストクラスでうまいこと使います。

AbstractMyClass.java
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() メソッドを呼び出しています。

AbstractMyClassTest.java
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 の利用で抽象クラスの偽装オブジェクトを作成し、抽象クラスに定義されたメソッドを呼び出すことができます。

AbstractMyClassTest.java
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 はモックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すように設定できるやつです。

AbstractMyClassTest.java
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 を使用すると、モックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すようになり、予期しないエラーが出たりするのであんまりオススメしません。

おわりに

他にも方法あるかな?

8
8
1

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
8
8