Kotlin Compiler Pluginのテストについて
はじめに
本記事では、Kotlin Compiler Pluginのテストについて紹介します。
Kotlin Compiler Pluginとは?
Kotlin Compiler Pluginとは以下の特徴があります。
- バイトコードを上書きできる
- コード記述の自動化ができる
- Kaptで使われている
Kotlin Compiler Pluginは、まだ安定版の機能ではなく、公式ドキュメントもありません。しかし、Kotlinのロードマップを確認したところ、Kotlin 1.5のターゲットではなさそうですが、Stable Compiler Plugin APIに向けて開発中のようです。
Kotlin Compiler Pluginを使ったライブラリの紹介
Kotlin Compiler Pluginの特徴を簡単に説明しましたが、ここからNoCopy Compiler Pluginライブラリを用いてKotlin Compiler Pluginの利用例を説明します。このライブラリでは、@NoCopy
アノテーションをセットしたデータクラスからcopy()
関数を削除したクラスを生成することで、copy()
関数を利用できないようにしています。
まずは次のUserデータクラスにcopy()
関数を利用することで任意のnameプロパティに変更できることを確認します。
fun main() {
val user = User(name = "tommykw")
println(user) // User(name=tommykw)
val copied = user.copy(name = "hoge")
println(copied) // User(name=hoge)
}
data class User(val name: String)
次にNoCopy Compiler Pluginの依存関係を追加した上で、Userデータクラスに@NoCopy
をセットして実行した場合、copy()
の箇所でコンパイルエラーになることがわかります。
fun main() {
val user = User(name = "tommykw")
println(user) // User(name=tommykw)
val copied = user.copy(name = "hoge") // Unresolved reference: copy
println(copied)
}
@NoCopy
data class User(val name: String)
ここまで、NoCopy Compiler Pluginを使ってKotlin Compiler Plugin APIの利用例について説明しました。
Kotlin Compiler Pluginのテスト
kotlin-compile-testingライブラリを利用することでKotlinで生成されたコードのテストを実施できます。引き続き、NoCopy Compiler Pluginを例にテストコードの説明をします。NoCopy Compiler Pluginでは次の通りテストを実施しています。
class NoCopyPluginTests {
@Rule
@JvmField
var temporaryFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `@NoCopy annotated non-data class should fail compilation`() {
val result = compile(kotlin("NonDataClass.kt",
"""
package dev.ahmedmourad.nocopy.compiler
import dev.ahmedmourad.nocopy.annotations.NoCopy
@NoCopy
class NonDataClass(val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("NonDataClass.kt: (3, 1): Only data classes could be annotated with @NoCopy!")
}
@Test
fun `@NoCopy annotated data class should compile just fine`() {
val result = compile(kotlin("DataClass.kt",
"""
package dev.ahmedmourad.nocopy.compiler
import dev.ahmedmourad.nocopy.annotations.NoCopy
@NoCopy
data class DataClass(val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
assertThat(result.messages).isEmpty()
}
private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation {
return KotlinCompilation().apply {
workingDir = temporaryFolder.root
compilerPlugins = listOf<ComponentRegistrar>(NoCopyPlugin())
inheritClassPath = true
sources = sourceFiles.asList()
verbose = false
jvmTarget = JvmTarget.JVM_1_8.description
}
}
private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result {
return prepareCompilation(*sourceFiles).compile()
}
}
最初のテストケースでは、クラスに対して@NoCopy
をセットした場合にコンパイルエラーとなります。もう一方のテストケースでは、データクラスに@NoCopy
をセットした場合にエラーにならず、コンパイルが通ります。
kotlin-compile-testingのcompile()
関数は以下のステップで実行されます。
- スタブを生成する
- aptを実行する
- Kotlinソースをコンパイルする
- Javaソースをコンパイルする
また、compile()
関数の戻り値はKotlinCompilation.Result
オブジェクトで、コンパイルの終了結果とコンパイル結果のメッセージなど取得できます。
inner class Result(
val exitCode: ExitCode,
val messages: String
) {
val classLoader = URLClassLoader(arrayOf(outputDirectory.toURI().toURL()),
this::class.java.classLoader)
val outputDirectory: File get() = classesDir
val sourcesGeneratedByAnnotationProcessor: List<File>
= kaptSourceDir.listFilesRecursively() + kaptKotlinGeneratedDir.listFilesRecursively()
val compiledClassAndResourceFiles: List<File> = outputDirectory.listFilesRecursively()
val generatedStubFiles: List<File> = kaptStubsDir.listFilesRecursively()
val generatedFiles: Collection<File>
= sourcesGeneratedByAnnotationProcessor + compiledClassAndResourceFiles + generatedStubFiles
}
最後に
NoCopy Compiler Pluginを基に、Kotlin Compiler Pluginのテストについて紹介させてもらいました。NoCopy Compiler Pluginに限らず、他のライブラリでも、kotlin-compile-testingを利用しているライブラリが多いので、テストコードを勉強していきたいと思います。
Discussion