JUnit5 + Mockito で enum を使った switch~case の分岐カバレッジを100%にするUTの書き方

# 発端
次のような Java の enum とそれを使用するクラスがあった時、JUnit5 で単体テストを実装し、JaCoCo で分岐カバレッジ100%を達成できません。

```java:Enum1.java
public enum Enum1 {
	VAL1, VAL2
}
```

```java:EnumStatement.java
public class EnumStatement {
	public static String toString(final Enum1 v) {
		String stringValue = null;
		switch (v) {
		case VAL1:
			stringValue = "V1";
			break;
		case VAL2:
			stringValue = "V2";
			break;
		}
		return stringValue;
	}
}
```

```java:EnumStatementTest.java
@TestInstance(PER_CLASS)
class EnumStatementTest {

	@ParameterizedTest
	@MethodSource
	void test(final Enum1 value, final String expected) {
			final String stringValue = EnumStatement.toString(value));
			assertThat(stringValue, is(expected));
		}
	}

	Stream<Arguments> test() {
		final var mockedEnum1Value = mock(Enum1.class);
		doReturn(2).when(mockedEnum1Value).ordinal();
		return Stream.of(
				arguments(Enum1.VAL1, "V1"),
				arguments(Enum1.VAL2, "V2"));
	}

}

```

JaCoCo はこの`switch`文のところで次のような状態(`1 of 3 branches missed.`)を報告してきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31769/aed0a9ba-9ab7-b7f6-9f1a-ed2d911264a0.png)

`v` の値が `VAL1` でも `VAL2` でもない時のケースがないということなのでしょうが、この　enum には二つの値しかないため、そんなパスを通すのは普通にテストを書いても実現できません。

アイデアとして、`Enum1` に不当を意味する値を定義して、`default` をこの `switch`文に追加するか、追加しないまでもUTで不当を意味する値でテストすることで、分岐カバレッジは100％にはなります。

しかしながら、本質的に enum にとって不必要な値を定義するのは、本来の処理の仕様として考えると無意味な選択です。それどころか後々思わぬ不具合の火種にすらなり得ます。

ですので、ここはUTでなんとか(強引に)もう一つの見えざるパスに分岐させることにします。

# UTクラスの改造

Java コンパイラが、euum を使った `switch~case` をどのようなコードにするかについては、 [ひしだまさんのサイト](https://www.ne.jp/asahi/hishidama/home/index.html "ひしだまのホームページ")の[記事](https://www.ne.jp/asahi/hishidama/home/tech/java/enum.html#h2_switch "switchの実装")に詳しく書かれていますので詳細は割愛しますが、ポイントは、`switch`文で`enum`を扱う時に、`enum`の`values()`が返す要素数、`case`文では`enum`の`ordinal()`の値に応じて分岐が制御されるようにJavaコンパイラがバイトコードを吐き出す点にあります。

つまり、通せていないパスは、`values()` が定義済みの値の数より一つ多い要素数の配列を返し、`ordinal()` が定義済みの値の順序番号よりも1つ大きい値を返すようにモック化することで通すことができます。

上の`EnumStatement`を将来のバグを防止する意味で改善したコードと、改造後のUTクラスは次のようになります。

まず、テスト対象のクラスです。

```java:EnumStatement.java
public class EnumStatement {
	public static String toString(final Enum1 v) {
		String stringValue = null;
		switch (v) {
		case VAL1:
			stringValue = "V1";
			break;
		case VAL2:
			stringValue = "V2";
			break;
		default:
			throw new IllegalStateException("Invalid Enum1 value!");
		}
		return stringValue;
	}
}
```

`default` を追加し、そこへ来ることがあった場合は例外をスローするようにしました。
これにより、将来`Enum1`に値が追加されたときに、この`switch~case`を適切に変更しないと、例外が発生することになり、何らかのテストで問題が発見されることになります。

続いてUTクラスです。

```java:EnumStatementTest.java
@TestInstance(PER_CLASS)
class EnumStatementTest {
	@ParameterizedTest
	@MethodSource
	void test(final Enum1 value, final String expected, final Class<IllegalStateException> exception) {
		final int valuesLength = Enum1.values().length;
		try (MockedStatic<Enum1> mockStatic = mockStatic(Enum1.class)) {
			mockStatic.when(Enum1::values).thenReturn(new Enum1[valuesLength + 1]);
			if (exception == null) {
				final String stringValue = assertDoesNotThrow(() -> EnumStatement.toString(value));
				assertThat(stringValue, is(expected));
			} else {
				final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> EnumStatement.toString(value));
				assertThat(thrown.getMessage(), is("Invalid Enum1 value!"));
			}
		}
	}

	Stream<Arguments> test() {
		final var mockedEnum1Value = mock(Enum1.class);
		final int valuesLength = Enum1.values().length;
		doReturn(valuesLength).when(mockedEnum1Value).ordinal();
		return Stream.of(
				arguments(Enum1.VAL1, "V1", null),
				arguments(Enum1.VAL2, "V2", null),
				arguments(mockedEnum1Value, "Invalid Enum1 value!", IllegalStateException.class));
	}
}

```

まず`values()` の返す値をモック化するには、Mockito の `mockStatic(Class<T>)`を使用します。

`values()`の戻す値は、中身はからでも良いので、本来の値の数より一つ大きい配列を作って、`thenReturn(new Enum1[<本当の要素数>+1])`のようにします。

:::note warn
  `<本当の要素数>`を`enum`の値が増えても、UT クラスの修正箇所が少なくなるよう可変にするために`enum`の`values()`を使うときは、`mockStatic()`の`try`文よりも前に書く必要があります。そうしないと Mockito が正しく処理できず UT がうまく動きません。要注意です。
:::

これでUTを実行するとめでたく、分岐カバレッジが100％(`All 3 branches coverd.`)になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31769/72632cae-9057-28f5-e0c4-d44f68f7ec0b.png)

## `switch`式でも同様
細かいことは書きませんが、`switch`式でも同様です。

```java:EnumExpression.java
public class EnumExpression {
	public static String toString(final Enum1 v) {
		return switch (v) {
		case VAL1 -> "V1";
		case VAL2 -> "V2";
		default -> throw new IllegalStateException("Invalid Enum1 value!");
		};
	}
}
```
```java:EnumExpressionTest.java
@TestInstance(Lifecycle.PER_CLASS)
class EnumExpressionTest {
	@ParameterizedTest
	@MethodSource
	void test(final Enum1 value, final String expected, final Class<IllegalStateException> exception) {
		final int valuesLength = Enum1.values().length;
		try (MockedStatic<Enum1> mockStatic = mockStatic(Enum1.class)) {
			mockStatic.when(Enum1::values).thenReturn(new Enum1[valuesLength + 1]);
			if (exception == null) {
				final String stringValue = assertDoesNotThrow(() -> EnumExpression.toString(value));
				assertThat(stringValue, is(expected));
			} else {
				final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> EnumExpression.toString(value));
				assertThat(thrown.getMessage(), is(expected));
			}
		}
	}

	Stream<Arguments> test() {
		final var mockedEnum1Value = mock(Enum1.class);
		final int valuesLength = Enum1.values().length;
		doReturn(valuesLength).when(mockedEnum1Value).ordinal();
		return Stream.of(
				arguments(Enum1.VAL1, "V1", null),
				arguments(Enum1.VAL2, "V2", null),
				arguments(mockedEnum1Value, "Invalid Enum1 value!", IllegalStateException.class));
	}
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31769/e0047175-5e4f-8394-a393-665c509f5f42.png)

`enum`を使用する`switch`式の場合は、`default`を書かない状態で、`enum`に値を増やすと、全ての値に対応していないとして`switch~case`文がコンパイルエラーになります。`switch`文では警告は出るもののコンパイルは通って動かせる状態にはなってしまうので、うっかり実装忘れたという場合でも、`switch`式の場合は早期発見することができる点が `switch`文の時とは大きく異なります。

:::note info
`default`をちゃんと書きなさい、`enum`の値は全部書きなさい、という話は今回の記事の本質とは違うので細かい議論は避けますw
:::

例えば次のように`VAL3`を増やしてみます。
```java:Enum1.java
public enum Enum1 {
	VAL1, VAL2, VAL3
}
```
この時の`default`を未定義の場合の、`EnumStatement`と`EnumExpression`は次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31769/82f0a421-50cf-45de-e3aa-8a829ae916d4.png)

EnumStatementの`switch`文のところには警告マークがついています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31769/f7010ea4-5f57-ae77-b112-8ac34d999bab.png)

そのい一方で、EnumExpressionの`switch`式のところにはエラーマークがついています。

警告とエラーの差は大きいですね！

# 最後に

ここで共有した内容は、Javaコンパイラが吐き出すバイトコードベースの内容であって、その内容が変わると期待通りに動作しなくなります。UTをコンパイラの実装に依存させることの善悪は一つの議論かと思いますので、ここでは割愛しますがご留意ください。

また、`switch`文に至るまでのコードパスの中で、`values()`や`ordinal()`が呼ばれるようなコードが幾度となくある場合、うまくモック化するのが難しいケースもあると思います。強引にやるとUTクラスが修正しにくい（わかりにくい）ものになりかねないので、そういった場合はおそらくテスト対象のクラス構成の見直しや、メソッド分割等のリファクタリングも視野に入れる必要があるのかもしれません。

:::note info
モックのメソッドが呼び出された回数で応答する値を変更することも可能なので、実装できないということはないと思います。

```java
  doReturn(1, 2, 3).when(...) 
```
```java
  mockStatick.when(...).thenReturn(1).thenReturn(2).thenReturn(3)
```
のようにすると、1回目、2回目、3回目以降、の返す値を指定したことになります。(`when`の`...` は書くのを端折ってるだけですw)

:::

「なんとか分岐網羅を100%にしなければならない」という時の一助になれば幸いです。