🐡

IntelliJ Plugin の開発で JUnit 5 を使う

2023/10/12に公開

IntelliJ Plugin の開発で JUnit 5 を使う

IntelliJ IDEA のプラグインは IntelliJ IDEA のウィーザードや IntelliJ Platform Plugin Template を使えば簡単に始められます。

しかし、単体テストが JUnit 3 とか 4 とかなんですね。とくに古いからって困ることはないんですけど、せっかくなので JUnit 5 にしてみます。

JUnit 5 化

build.gradle.kts の修正

標準で JUnit 5 が有効になっていませんので、build.gradle.kts ファイルを修正します。

diff --git a/build.gradle.kts b/build.gradle.kts
index ab1ec21..f319ca5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,7 +20,16 @@ intellij {
     plugins.set(listOf(/* Plugin Dependencies */))
 }
 
+dependencies {
+    testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2")
+    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2")
+}
+
 tasks {
+    test {
+        useJUnitPlatform()
+    }
+
     // Set the JVM compatibility versions
     withType<JavaCompile> {
         sourceCompatibility = "17"

テストケースを書く

JUnit 5 ではアノテーションやアサーションが変更されています。

src/test/kotlin/com/example/demo/Test.kt
package com.example.demo

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class Test {
    @Test
    fun test() {
        assertTrue(false)
    }
}

IntelliJ の機能テストを書く

IntelliJ の機能テストはドキュメントが少なくて手探り状態でした。

まず、IntelliJ の機能テストのためには様々なサービスが登録されていなければなりません。テスト用にそれらを初期化するために TestApplicationManager.getInstance() メソッドを呼び出す必要があります。

src/test/kotlin/com/example/demo/Test.kt
package com.example.demo

import com.intellij.testFramework.TestApplicationManager
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class Test {
    @Test
    fun test() {
        assertTrue(false)
    }

    companion object {
        init {
            TestApplicationManager.getInstance()
        }
    }
}

テスト用の IntelliJ プロジェクトを読み込みます。

@Test
fun test() {
    val projectRoot = System.getProperty("user.dir")
    val project = PlatformTestUtil.loadAndOpenProject(Path(projectRoot)) {}
    // 第二引数 (`{}`) はプロジェクトを閉じたときに実行されるクロージャー (多分)
}

プロジェクトからファイルを読み込むには VirtualFilePsiFile への理解が欠かせません。(私はよくわかってません)

@Test
fun test() {
    val projectRoot = System.getProperty("user.dir")
    val project = PlatformTestUtil.loadAndOpenProject(Path(projectRoot)) {}
    val virtualFile = VfsTestUtil.findFileByCaseSensitivePath(
        Paths.get(projectRoot, "src/test/kotlin/com/example/demo/Test.kt").toString()
    )
    val psiFile = PsiManager.getInstance(project).findFile(virtualFile)!!

    println(psiFile.fileType.name)
    println(psiFile.text)
}

このテストを実行すると、ERROR: Read access is allowed from inside read-action (or EDT) only (see com.intellij.openapi.application.Application.runReadAction()) というエラーになります。

自分もあまりよくわかってないのですが、UI スレッドでないスレッドから呼び出せないメソッドが含まれているようです。

次のように runReadAction() メソッドのクロージャー内で処理すればいいのですが、テストのたびに書いていたら結構面倒です。

ApplicationManager.getApplication().runReadAction {
    // ここに処理を書く
}

代わりに、テストクラスを拡張します。

@ExtendWith(EdtInterceptor::class)
class Test {
    // ...

これでエラーがなくなりました。

ファイルタイプとファイルの内容がコンソールに出力されます。(ファイルタイプが PLAIN_TEXT になってるのが残念ですが、どうすればいいのかはいまいちわかりません)

ファイルの内容を書き換えるには、Document クラスを利用します。

@Test
fun test() {
    val projectRoot = System.getProperty("user.dir")
    val project = PlatformTestUtil.loadAndOpenProject(Path(projectRoot)) {}
    val virtualFile = VfsTestUtil.findFileByCaseSensitivePath(
        Paths.get(projectRoot, "src/test/kotlin/com/example/demo/Test.kt").toString()
    )
    val psiFile = PsiManager.getInstance(project).findFile(virtualFile)!!

    println(psiFile.fileType.name)
    println(psiFile.text)

    val document = PsiDocumentManager.getInstance(project).getDocument(psiFile)!!
    // 0 文字目から 10 文字目までを削除して "abc" という文字列を挿入します
    document.replaceString(0, 10, "abc")

    println(document.text)
}

これを実行すると ERROR: Assertion failed: Write access is allowed inside write-action only (see com.intellij.openapi.application.Application.runWriteAction()) エラーになります。

WriteCommandAction クラスを利用して、書き込みを許可したうえで書き換えるようにします。

@Test
fun test() {
    // ...

    val document = PsiDocumentManager.getInstance(project).getDocument(psiFile)!!

    WriteCommandAction.writeCommandAction(project).run<Exception> {
        document.replaceString(0, 10, "abc")
    }

    println(psiFile.text)
}

無事、書き換えられました。

最後に

textlint IntelliJ Plugin の開発で機能テストも行っていますが、ドキュメントが少なくて手探り状態ですすめています。

これがいいやり方かどうかは分かりませんが、参考になればと思い公開しました。間違いや別の方法があれば教えていただけるとうれしいです。

Discussion