🐟

JUnit4 から JUnit5 へ移行する方法

2021/06/20に公開

ここ最近 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 の場合は適当によみかえてください[1]

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

pom.xml(JUnit4)
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

上記の内容の代わりに以下をいれます

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 だったら入れなくても良いのかなとおもいました。

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>

ちなみにそれぞれの意味は, 公式の説明 を参考に以下な感じです。必要になったときに, 他のものも入れていく感じですね。
<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 とか。詳細は公式へ)</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) を使ってたかと思いますが(以下みたいな)

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 でないクラスを用いて階層化ができます。
記法が素直になったかなと思いました(所感)。

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 をしている場合はエラーになります。

この場合は, 公式記載の内容に従って, @TestInstance(Lifecycle.PER_CLASS) を対象のクラスにつけてあげると, テストインスタンスがテストクラスごとに作成されるようになり, inner class で @BeforeAll, @AfterAll が使えるようになります。

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 を使ってたかなと思います。

JUnit4の場合
@Test(expected = NullPointerException.class)
public void NullPointerExceptionが発生する() {
    /* NullPointerException の発生を期待するテスト */
}
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 式経由)を渡します。

JUnit5の場合
@Test
public void NullPointerExceptionが発生する() {
    assertThrows(NullPointerException.class, () -> {
        /* NullPointerException の発生を期待するテスト */
    });
}

例外の発生チェックのみであれば assertThrows のみで大丈夫ですが, エラーメッセージ含み発生した Exception の詳細をテストする場合は, assertThrows が返す Exception を受け取って諸々の検証を書きます。

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

TemporaryFolder を用いた試験の書き方の変更

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

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

@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

private Path root;

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

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

@TempDir
Path root;

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

そのほか

  • データセットに対する繰り返し試験(JUnit4 は @RunWith(Theories.class) + @DataPoint + @Theory だったもの)
    • JUnit5 では: 繰り返し対象がプリミティブ型の場合は, @ParameterizedTest + @ValueSource
    • JUnit5 では: 繰り返し対象がオブジェクトの場合は, @ParameterizedTest + @MethodSource
脚注
  1. Gradle の設定は, 公式の https://github.com/junit-team/junit5-samples/tree/main/junit5-migration-gradle が参考になるかと ↩︎

Discussion