内部で使っているライブラリを JUnit 5 (jupiter) に 移行した

この記事では、JUnit5への移行を行うと共に どういう書き換えをしたかを書いておきます。

以下の内容で書いていきます。

  • 今回移行したプロジェクトの前提
  • どういう方法で移行するか
  • PMDのアップデート
  • junit 4系の依存を完全に外す
  • junit-jupiterの依存とテストの設定を追加
  • アノテーション周りをsedで置換
  • @Test(expected = HogeException.class) を書き換える
  • アサーション周りをAssertJで書き直す
  • Wiremockの移行
  • Mockito周りの書き換え
  • まとめ

今回移行したプロジェクトの前提

  • @Category を使ってない
  • @AfterClass, @BeforeClass を使ってない
  • @RunWith(Enclosed.class) を使ってない
  • アサーションは基本的にAssertJを使っている
  • Gradleでビルドされている小さいライブラリ
  • 自分達でRuleは書いてない
  • SpringやMockitoのバージョンは最新に維持されていること

どういう方法で移行するか

今回は小さいプロジェクトの移行だったので、ガッとやりました。 本当はもうちょっとスモールステップで切り替えた方が良いのかなぁと思いつつ そんな規模のプロジェクトだと、色々ありそうで険しそうだなぁって気持ちになりました。

  • PMDのアップデート
  • junit 4系の依存を完全に外す
  • junit-jupiterの依存とテストの設定を追加
  • アノテーション周りをsedで置換
  • @Test(expected = HogeException.class) を書き換える
  • アサーション周りをAssertJで書き直す
  • Wiremockの移行
  • Mockito周りの書き換え

PMDのアップデート

PMDのアップデートをしましょう。 JUnit4 向けの検査で false positiveになります。

Issueとしては以下になります。

6.7.0で対応されているので 6.7.0以降にアップデートしましょう

JUnit 4系の依存を完全に外す

Gradleを使っているので、以下の記述を追加して、JUnit 4系の依存を外します。

configurations.all {
    exclude group: 'junit', module: 'junit'
}

なぜ外すかというとわかりやすく移行するためです。

junit-jupiterの依存とテストの設定を追加

build.gradleに、下の設定を追加してください。 ちなみに、この機能はGradle 4.6に追加されたもののようです。

test {
    useJUnitPlatform()
}

また、junit-jupiterの依存を追加しておきます。 以下の junit-jupiter は推移依存に junit-jupiter-params が入っており 依存関係としてはシンプルで便利なので、おすすめです。

dependencies {
    testCompile "org.junit.jupiter:junit-jupiter:5.4.2"
}

アノテーションsedで置換

下の記事にも書かれていますが、大体のアノテーションは置き換えるだけで動きます。

今回はひとまず、sedで置換します。(Macで動かしていて、gnu sedではないので必要に応じて読み替えてください) ここでは、MockitoやSpringのRunnerも同時に書き換えています。必要に応じて飛ばしてください。

git ls-files '*.java' | xargs sed -i '' 's/import org.junit.runner.RunWith/import org.junit.jupiter.api.extension.ExtendWith/g'
git ls-files '*.java' | xargs sed -i '' 's/@RunWith(SpringJUnit4ClassRunner.class)/@ExtendWith(SpringExtension.class)/g'
git ls-files '*.java' | xargs sed -i '' 's/@RunWith(SpringRunner.class)/@ExtendWith(SpringExtension.class)/g'
git ls-files '*.java' | xargs sed -i '' 's/import org.springframework.test.context.junit4.SpringJUnit4ClassRunner/import org.springframework.test.context.junit.jupiter.SpringExtension/g'
git ls-files '*.java' | xargs sed -i '' 's/import org.springframework.test.context.junit4.SpringRunner/import org.springframework.test.context.junit.jupiter.SpringExtension/g'
git ls-files '*.java' | xargs sed -i '' 's/@RunWith(MockitoJUnitRunner.class)/@ExtendWith(MockitoExtension.class)/g'
git ls-files '*.java' | xargs sed -i '' 's/import org.mockito.junit.MockitoJUnitRunner/import org.mockito.junit.jupiter.MockitoExtension/g'

git ls-files '*.java' | xargs sed -i '' 's/import org.junit.After/import org.junit.jupiter.api.AfterEach/g'
git ls-files '*.java' | xargs sed -i '' 's/@After/@AfterEach/g'

git ls-files '*.java' | xargs sed -i '' 's/import org.junit.Before/import org.junit.jupiter.api.BeforeEach/g'
git ls-files '*.java' | xargs sed -i '' 's/@Before/@BeforeEach/g'

git ls-files '*.java' | xargs sed -i '' 's/import org.junit.Test/import org.junit.jupiter.api.Test/g'
git ls-files '*.java' | xargs sed -i '' 's/@Test/@Test/g'

git ls-files '*.java' | xargs sed -i '' 's/import org.junit.Ignore/import org.junit.jupiter.api.Disabled/g'
git ls-files '*.java' | xargs sed -i '' 's/@Ignore/@Disabled/g'

ここでコンパイルエラーになることはあると思いますが 置いておきます。

@Test(expected = HogeException.class) を書き換える

@Test(expected = HogeException.class) を書き換えます。 junit-jupiterからは、アノテーションで投げられる例外の宣言ができなくなっています。 そのため、junit-jupiterのアサーションではassertThrows もしくは、AssertJのassertThatThrownByなどを使ってください。

junit-jupiterのアサーションに関しては以下の記事が参考になると思います。

コンパイルエラーから見つけて直す形で書き換えていきました。

アサーション周りをAssertJで書き直す

JUnit4のアサーション周りを使っていたコードをひたすら直します。 コンパイルエラーになっているはずなので、Intellij IDEAで書き換えてはコンパイルを繰り返します。 この辺は頑張って書き換えました。

今回は使いませんでしたが、JUnit5のアサーションを使う場合は、 assertThatなどの引数が入れ替わっていたりするので書き換えましょう。

wiremockの移行

一番悩みました。WireMock本体からはExtensionが出されていません。 そのため、WireMockRuleを使ったテストを書き換える必要があります。 正直、BeforeEach/AfterEachでWireMockServerをstart/stopするのでも良いのですが 書いているテストの量が多い場合は難しい気がします。

今回は、3rd partyのモジュールを使うことにしました。 その理由としては、先程も書いた通り、3rd partyのモジュールを使ったとしても 自分で書き換えるのは苦ではないかな、というところで考えています。

使ったのは以下のライブラリです。

以下の形でテストコードで使っている @Rule を書き換えると良いと思います。

- @Rule
- WireMockRule wireMock = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort());
+ @Managed
+ WireMockServer wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());

依存関係としては以下の記述を追加しました。

repositories {
    // ... 省略
+    maven { url "https://jitpack.io/" } // for wiremock-extension
}

dependencies {
+   testCompile("com.github.JensPiegsa:wiremock-extension:0.4.0") {
+       exclude module: "wiremock"
+   }
}

Mockito周りの書き換え

依存関係として、 mockito-junit-jupiter を 追加してください。 MockitoExtensionが別のモジュールとして追加されています。

dependencies {
    testCompile "org.mockito:mockito-core:$mockitoVersion"
+    testCompile "org.mockito:mockito-junit-jupiter:$mockitoVersion"
}

テストのライフサイクルが小さくなってしまいます。そのため、いままで通っていたテストが通らなくなります。 どういうテストが通らなくなるかというと、以下のようなテストです。 一部のテストで使う mockのセットアップが UnnecessaryStubbingとして検出されるようになります。

@ExtendWith(MockitoExtension.class)
public class CustomHealthIndicatorTest {

    @Mock
    Health.Builder builder;

    @BeforeEach
    public void setUp() throws Exception {
        when(builder.up()).thenReturn(builder);
        when(builder.down()).thenReturn(builder);
    }

    @Test
    public void testUp() throws Exception {
        builder.up(); // 実際にはテスト対象のメソッドを読んでいる
    }

    @Test
    public void testDown() throws Exception {
        builder.down(); // 実際にはテスト対象のメソッドを読んでいる
    }

}

このテストの場合は、2つしかないので、それぞれをテストにインライン化するだけで済みますが 複数のテストがある場合、テストの書き方を工夫する必要があるかなと思います。

例えば、@Nested を使って、それぞれのmockが必要なテストを分離します。

@ExtendWith(MockitoExtension.class)
public class CustomHealthIndicatorTest {

    @Nested
    class Up {
        
        @BeforeEach
        public void setUp() {
            when(builder.up()).thenReturn(builder);
        }
        
        @Test
        public void testStreamExists() throws Exception {
            builder.up();
        }
        
    }

    @Nested
    class Down {
        // 省略
    }
}

この挙動ですが、実はIssueに上がっています。 Issueとしては下のリンクのものです。

元の挙動のほうが移行する場合は楽なのですが、Issueが立っている以上、今のタイミングでは仕方ないので諦めました。 テストコードを見直す機会になっても良いと思います。

まとめ

正直今回はすぐに切り替えられました。Mockitoの1系から2系への移行よりも楽だった認識です。

今回移行したライブラリはそんな大きなモジュールではありませんでした。 そのため、すぐに移行することが出来ました。 また、Deprecatedなクラスや廃止される機能をできるだけ使わずに、基本的にアップデートした際に書き換える方針でやっていたので そもそも移行する手間が少なかった、というのも有ると思います。

皆さんも junit-jupiter で移行していきましょう。 @ParameterizedTest が最高に気持ちいいので移行するの、オススメです。

今回はライブラリということもあり、大きなテストもなくスッとあげられましたが これから、もっと他のサーバーサイドアプリケーションに展開していきます。 他に分かったことがあれば、別途記事を書きます。

それではまた。