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

# はじめに

知識のおすそ分けです。

# 目次

- [単体テスト対象のメソッド in 抽象クラス](#単体テスト対象のメソッド-in-抽象クラス)
- [抽象クラスに単体テストを書くときのハードル](#抽象クラスに単体テストを書くときのハードル)
- [抽象クラスに単体テストをあてる方法５つを紹介します](#抽象クラスに単体テストをあてる方法５つを紹介します)
  - 方法１．[抽象クラスが継承されている具象クラスに単体テストを書く](#方法１抽象クラスが継承されている具象クラスに単体テストを書く)
  - 方法２．[抽象クラスを継承する具象クラスを新たに作成して単体テストを書く](#方法２抽象クラスを継承する具象クラスを新たに作成して単体テストを書く)
  - 方法３．[テストクラスで匿名クラスを作成して単体テストを書く](#方法３テストクラスで匿名クラスを作成して単体テストを書く)
  - 方法４．[`Mockito.Spy` を使う](#方法４mockitospy-を使う)
  - 方法５．[`Mockito.CALLS_REAL_METHODS` を使う](#方法５mockitocalls_real_methods-を使う)
- [おわりに](#おわりに)

# 開発環境


- JUnit 5
- Java 8 以降


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

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

```java: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: < 抽象クラスはインスタンス化できない...。

```java:AbstractMyClassTest.java
public class AbstractMyClassTest {

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

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

- 方法１．[抽象クラスが継承されている具象クラスに単体テストを書く](#方法１抽象クラスが継承されている具象クラスに単体テストを書く)
- 方法２．[抽象クラスを継承する具象クラスを新たに作成して単体テストを書く](#方法２抽象クラスを継承する具象クラスを新たに作成して単体テストを書く)
- 方法３．[テストクラスで匿名クラスを作成して単体テストを書く](#方法３テストクラスで匿名クラスを作成して単体テストを書く)
- 方法４．[`Mockito.Spy` を使う](#方法４mockitospy-を使う)
- 方法５．[`Mockito.CALLS_REAL_METHODS` を使う](#方法５mockitocalls_real_methods-を使う)

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

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

```java:MyClass.java
public class MyClass extends AbstractMyClass {

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

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

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

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

```java:MyClassTest.java
public class MyClassTest {

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

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


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

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

```java:ConcreteMyClass.java
public class ConcreteMyClass extends AbstractMyClass {

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

}
```

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


```java:ConcreteMyClassTest.java
public class ConcreteMyClassTest {

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

}
```

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

## 方法３．テストクラスで匿名クラスを作成して単体テストを書く

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

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

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

```java: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"));
	}

}
```



:::note warn
注意点：
匿名クラスは再利用できないので、単体テストで使用する場合はテストメソッドごとに毎回新しい匿名クラスのインスタンスを作成する必要があります。いちおう、`@BeforeEach` アノテーションなどを使った `setUp()` メソッド内でインスタンス化をすればコードの重複は避けることができます。
:::

## 方法４．`Mockito.Spy` を使う

`@Spy` の利用は、テスト対象の抽象クラスが具体的な実装を持つメソッドを含む場合にけっこう便利な方法です。`@Spy` の利用で抽象クラスの偽装オブジェクトを作成し、抽象クラスに定義されたメソッドを呼び出すことができます。

```java: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` よりも有効であると言えます。

:::note
抽象クラスに実装が定義されたメソッドの単体テストでは `@Mock` ではなく `@Spy` を使おう。
:::

## 方法５．`Mockito.CALLS_REAL_METHODS` を使う

`CALLS_REAL_METHODS` はモックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すように設定できるやつです。

```java: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()` メソッドの振る舞いを確認する単体テストを行うことができます。

:::note warn
注意点：
`CALLS_REAL_METHODS` を使用すると、モックオブジェクトのすべてのメソッドがデフォルトの実装を呼び出すようになり、予期しないエラーが出たりするのであんまりオススメしません。
:::

# おわりに

他にも方法あるかな？