JUnit4 から JUnit5 へ移行する方法

ここ最近 JUnit4 から JUnit5 に移行する方法を模索していたのですが, あっちへこっちへ資料を捜索して大変だったのでまとめることにしました

## 移行の進め方の方針
小さなプロジェクトだと, さっくり JUnit4 から JUnit5 へ記法を一気に変えることもできるかなと思うのですが, 大きいプロジェクトになると物量が厳しいので, 基本的には以下のように進めるのが安定かなと思ってます。

1. JUnit のライブラリを更新する  
JUnit4 を削除し, JUnit5 を追加する（※ ただし, JUnit4 実行を維持するために, junit-vintage-engine なるものも入れる）
2. JUnit4 記法の既存のテストを JUnit5 の記法に書き換える  
（新しいテストは JUnit5 の記法で書いてく）
3. 全てのテストが JUnit5 の記法になったら, junit-vintage-engine を依存から削除する


上記 2 番目は, junit-vintage-engine が依存ライブラリに入っているかぎりやらなくても動作するけど, いずれやらねばならぬときは来るのではと思うので, 好きなタイミングで。

ちなみに JUnit の公式の移行方法は → https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4

## 具体的な進め方

### ライブラリの更新
まずは, JUnit5 のライブラリに切り替えます（以下は Maven なんですが, Gradle の場合は適当によみかえてください[^gradle]）
[^gradle]: Gradle の設定は, 公式の https://github.com/junit-team/junit5-samples/tree/main/junit5-migration-gradle が参考になるかと

もともと JUnit4 で以下のような感じで書いていると思うので

``` xml:pom.xml（JUnit4）
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
```
上記の内容の代わりに以下をいれます
 
``` xml:pom.xml（JUnit5）
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
```

ちなみに僕は InteliJ 使ってるので以下もいれました
（ただ公式のメッセージ `Only needed to run tests in a version of IntelliJ IDEA that bundles older versions ` をみると, 新しい InteliJ だったら入れなくても良いのかなとおもいました。

``` xml:pom.xml（JUnit5）
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.7.0</version>
    <scope>test</scope>
</dependency>
```


ちなみにそれぞれの意味は, [公式の説明](https://junit.org/junit5/docs/current/user-guide/#dependency-metadata) を参考に以下な感じです。必要になったときに, [他のもの](https://junit.org/junit5/docs/current/user-guide/#dependency-metadata)も入れていく感じですね。
<dl>
  <dt>org.junit.jupiter の junit-jupiter-engine</dt>
  <dd>JUnit Jupiter のテストエンジン（実行時）</dd>
  <dt>org.junit.jupiter の junit-jupiter-api</dt>
  <dd>テスト記述のための各種 API（`@Test` とか `@BeforeEach` とか。[詳細は公式へ](https://junit.org/junit5/docs/current/user-guide/#writing-tests)）</dd>
  <dt>org.junit.vintage の junit-vintage-engine</dt>
  <dd>JUnit3 と JUnit4 を実行可能にするテストエンジン</dd>
</dl>


### JUnit4 → JUnit5 へのテストの書き換え
個人的には JUnit4 の `@Rule` がなくなったのが, なかなかでした

#### 単純な書き換え 1 （Package 変更）
JUnit4 で `org.junit` 配下のものを使っていたのを, JUnit5 では `org.junit.jupiter.api` 配下のものになるので単純に import の Package を書き換えます

| 対象 | JUnit4 Package | JUnit5 Package |
|:-----------:|:------------:|:------------:|
| `@Test` とか | org.junit.* | org.junit.jupiter.api.* |
| `fail()` とか | org.junit.Assert.* | org.junit.jupiter.api.Assertions.* |

#### 単純な書き換え 2 （アノテーションの置換）
アノテーションが変更になっているので, 置き換えます

| JUnit4 | JUnit5 |
|:------------|:------------|
| `@Before` | `@BeforeEach` |
| `@After` | `@AfterEach` |
| `@BeforeClass` | `@BeforeAll` |
| `@AfterClass` | `@AfterAll` |
| `@Ignore` | `@Disabled` |
| `@Category` | `@Tag` |
※ ただし, テストの階層化などで inner class で `@BeforeClass` や `@AfterClass` してる場合は, もう少しアノテーションが入用なので, 別途下の方にかきます）

なお, 単純な書き換えでは済まないやつは, 以下とかです 

- `@RunWith` から `@ExtendWith` への切り替え
- `@Rule` や `@ClassRule` から, `@ExtendWith` や `@RegisterExtension` への切り替え

#### テストの階層化の方法の変更
JUnit4 ではテストの整理には inner class と `@RunWith(Enclosed.class)` を使ってたかと思いますが（以下みたいな）

``` java:JUnit4テストの整理方法
@RunWith(Enclosed.class)
public class SampleJUnit4Test {

  public static class TestCategoryA {
    @Test
    public void testA1() { ... }
  }

  public static class TestCategoryB {
    @Test
    public void testB1() { ... }
  }
}
```

JUnit5 では `@Nested` と static でないクラスを用いて階層化ができます。
記法が素直になったかなと思いました（所感）。

``` java:JUnit5テストの整理方法
public class SampleJUnit5Test {

  @Nested
  class TestCategoryA {
    @Test
    public void testA1() { ... }
  }

  @Nested
  class TestCategoryB {
    @Test
    public void testB1() { ... }
  }
}
```
注意事項としては, JUnit4 から JUnit5 への移行作業中に `@Nested` をつけたものの static class にしたままだと, コンパイルエラーにはならないのですがテスト実行が無視される事象が発生するのでご注意ください。（詳細は調べてないです）

##### inner class で BeforeAll, AfterAll をしている箇所の対応
手前で, 以下のように書いていたかと思いますが, 
>※ ただし, テストの階層化などで inner class で @BeforeClass や @AfterClass してる場合は, もう少しアノテーションが入用なので, 別途下の方にかきます）

JUnit5 の移行時に, `@Nested` を用いたテスト階層化にすることで, inner class が static ではなくなります。このため, inner class で `@BeforeClass` や `@AfterClass` をしている場合はエラーになります。

この場合は, [公式記載](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle)の内容に従って, `@TestInstance(Lifecycle.PER_CLASS)` を対象のクラスにつけてあげると, テストインスタンスがテストクラスごとに作成されるようになり, inner class で `@BeforeAll`, `@AfterAll` が使えるようになります。

``` java:JUnit5で内部クラスでAll系を使う
public class SampleJUnit5Test {

  @Nested
  @TestInstance(Lifecycle.PER_CLASS)
  class TestCategory {

    @BeforeAll
    static void beforeAll() { ... }
  }
}
```

#### Exception の Assert 方法の変更
Exception 系の Assert 方法が変更になったのでそこも変えます。
JUnit4 だと, `@Test(expected = SampleException.class)` や, `ExpectedException` を使ってたかなと思います。

``` java:JUnit4の場合
@Test(expected = NullPointerException.class)
public void NullPointerExceptionが発生する() {
    /* NullPointerException の発生を期待するテスト */
}
```
``` java:JUnit4の場合
@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void SampleExceptionが発生する() {
    expectedException.expect(SampleException.class);
    expectedException.expectMessage("SampleException のメッセージ");

    /* SampleException の発生を期待するテスト */
}
```

JUnit5 の場合は `assertThrows` を使います。
`assetThrows` の引数に, 発生を期待する Exception の class と, テストコード（lambda 式経由）を渡します。

``` java:JUnit5の場合
@Test
public void NullPointerExceptionが発生する() {
    assertThrows(NullPointerException.class, () -> {
        /* NullPointerException の発生を期待するテスト */
    });
}
```
例外の発生チェックのみであれば `assertThrows` のみで大丈夫ですが, エラーメッセージ含み発生した Exception の詳細をテストする場合は, `assertThrows` が返す Exception を受け取って諸々の検証を書きます。

``` java:JUnit5の場合
@Test
public void SampleExceptionが発生する() {
    SampleException exception = assertThrows(SampleException.class, () -> {
        /* SampleException の発生を期待するテスト */
    });
    assertEquals("SampleException のメッセージ", exception.getMessage());
}
```


#### TemporaryFolder を用いた試験の書き方の変更
JUnit4 ではファイル周りの試験などのために `@Rule` + `TemporaryFolder` を使って試験を書いてる箇所があるかなと思います。JUnit5 では `@Rule` がいなくなったのでこちらも修正対象です。

といっても非常に単純で, JUnit4 で以下のように書かれていた内容を

``` java:
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

private Path root;

@Before
public void setup() {
    root = temporaryFolder.getRoot().toPath().toAbsolutePath();
}
```

以下の感じに直す（なお, `@TempDir` がつく変数は `private` だとエラーになるので注意）

``` java:
@TempDir
Path root;
```

これ, かなり煩わしいものが消えて良い感じになったきがします。

#### そのほか
* データセットに対する繰り返し試験（JUnit4 は `@RunWith(Theories.class)` + `@DataPoint` + `@Theory` だったもの）
    * JUnit5 では: 繰り返し対象がプリミティブ型の場合は, `@ParameterizedTest` + `@ValueSource`
    * JUnit5 では: 繰り返し対象がオブジェクトの場合は, `@ParameterizedTest` + `@MethodSource`