PSI Tests for IntelliJ Plugin Developers

Often, plugin developers spend a lot of time figuring out their way around the PSI. The PSI APIs provide an elegant way to gather information, generate or manipulate source code. Though the PsiViewer Plugin comes in handy during development, it does not replace good old tests.

IntelliJ's testing framework serves a variety of needs. However, this post will only focus on testing logic that involves the PSI.

This post is to intended for a couple of purposes,

  1. I spent almost a day figuring out how to write tests for PSI traversal logic. With this information, you don't have to break a sweat.
  2. After trying out many approaches, this is the best way forward for me. I am curious to find out how others are approaching the same problem.

LightIdeaTestCase for PSI Tests

Most classes I came across in com.intellij.testFramework package use JUnit 3, which includes LightIdeaTestCase. Therefore, your PSI tests should be run by JUnit 3. That's a no-brainer.

The best way forward would be to bring in JUnit 5 (if you haven't already) for your tests because a preliminary search revealed no way to mix JUnit 3 tests with JUnit 4.

However, JUnit 5's Vintage Engine is capable of running both JUnit 3 and JUnit 4 tests. Yay!

First, ensure you have JUnit 5 setup correctly with at least the following dependencies.

testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.2") // 🔥
build.gradle.kts

JUnit 3 Tests

JUnit 3 existed even before Java added annotations to the language spec. Therefore, these tests look slightly different.

  1. They extend from a base class, usually junit.framework.TestCase.
  2. The test method/function should be public and start with a test prefix.

These are the two things to remember before writing PSI tests.

Canary PSI Test

import com.google.common.truth.Truth.assertThat
import com.intellij.psi.PsiFileFactory
import com.intellij.testFramework.LightIdeaTestCase
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.psi.KtFile

class CanaryPsiTest : LightIdeaTestCase() {
  fun testCreatePsiFromPlainText() {
    // given
    val fileName = "Hello.kt"
    val source = """
      fun hello() {
        println("Hello!")
      }
    """.trimIndent()

    // when
    val psiFile = PsiFileFactory
      .getInstance(project)
      .createFileFromText(fileName, KotlinLanguage.INSTANCE, source)

    // then
    assertThat(psiFile)
      .isInstanceOf(KtFile::class.java)
  }
}

The test extends LightIdeaTestCase, which is a descendent of JUnit 3's TestCase. The test function's name  testCreatePsiFromPlainText begins with the test prefix.

The PsiFileFactory can create a PsiFile instance from any Language you provide.

That's all you need to bring your PSI business logic under test for your IntelliJ Plugins. Do you test your PSI logic? If yes, how? Let me know @ragunathjawahar.