mike-neckのブログ

Java or Groovy or Swift or Golang

Kotlin の基礎を身につけるための学習素材を作ってみた

Kotlin を使ったプログラムを書く場合には以下の要素によってプログラムを書く速さが決まってくる

続きを読む

あなたの知らない Gradle の kotlin-dsl プラグイン

やんくさんに頼まれたので、誰かが書くだろうと思って待ってたけど誰も書かない Gradle の kotlin-dsl プラグインについて書きます。


まず、先日(2020/12/11)開催された JJUG ナイトセミナーにて、時間がないのでほぼ飛ばしてしまった、こちらの話題について解説します。

f:id:mike_neck:20201212182915p:plain

build.gradle(build.gradle) の中で if 文(Kotlin だと式ですね)とか for 文(こっちは式だったっけ?)を書くと、宣言的なコード(例えば依存ライブラリーの宣言など)と命令的なコードが混在してしまいます。そのため、ビルドスクリプトを読むために頭の切り替えが必要になるのでメンテナンス性が下がるというのが理由です。したがって、命令的なコードを書いている部分はすべてプラグインにして、 buildSrc プロジェクトに移動するのがよいと公式のドキュメントに書いてあります。

docs.gradle.org

しかし、これは素直におすすめできないところがあって、プラグインを書くためには少しだけプラグインの仕組みの学習コストがかかります(まあ、大したコストではありませんが…) 。そこで今持っている build.gradle(build.gradle.kts) の 知識でプラグインを無理なく書ける仕組みを使うのがよいでしょう。

プリコンパイルド・スクリプトプラグイン(Precompiled script plugins) というのがその仕組です。

docs.gradle.org

具体的には以前このブログで紹介した groovy-gradle-plugin が Groovy の場合のプラグインです。ここでは詳しく解説しないので、以下のエントリーを読んでください。

mike-neck.hatenadiary.com

これの Kotlin 版が kotlin-dsl プラグインです。(この言い方は語弊があって、先に kotlin-dsl があって、その後に Groovy 版ができたらしい)

なお、ググラビリティが悪いので、このプラグインの名前はどうにかしてほしい


kotlin-dsl の書き方

buildSrc/build.gradle.kts ファイルに以下のように書きます。 repositories {} ブロックを忘れると kotlin-dsl をビルドするための Kotlin をダウンロードできなくなってエラーが発生します

plugins {
  `kotlin-dsl`
}

repositories {
  jcenter()
}

buildSrc/src/main/kotlin の下に任意の名前で .gradle.kts ファイルを作成します。ここではサンプルのため、 example.gradle.kts というファイル名にします。なお、内容的には先の Java toolchains のエントリーの build.gradle と同じ様な内容にしています

plugins {
  `java`
}

repositories {
  mavenCentral()
}

dependencies {
  compileOnly("org.jetbrains:annotations:19.0.0")
  implementation("org.slf4j:slf4j-api:1.7.30")
  testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
  testImplementation("org.assertj:assertj-core:3.18.1")
}

internal val ss = sourceSets
internal val testSourceSets = ss.getByName("test")
@Suppress("UnstableApiUsage")
internal val javaToolChains = extensions.getByType<JavaToolchainService>()
@Suppress("UnstableApiUsage")
internal val javaPluginExt = extensions.getByType<JavaPluginExtension>()

javaPluginExt.toolchain {
  @Suppress("UnstableApiUsage")
  languageVersion.convention(JavaLanguageVersion.of(8))
  @Suppress("UnstableApiUsage")
  vendor.convention(JvmVendorSpec.AZUL)
  @Suppress("UnstableApiUsage")
  implementation.convention(JvmImplementation.VENDOR_SPECIFIC)
}

open class ProjectTestConfig {
  @Suppress("UnstableApiUsage")
  // 指定されたバージョンの Java でテストを実行するタスクを登録します
  fun testOnJavaVersion(vararg javaVersions: Int): Map<Int, TaskProvider<Test>> =
    javaVersions.map { javaVer ->
      javaVer to tasks.register<Test>("testJava${javaVer}") {
        group = "verification"
        description = "runs test on Java $javaVer"
        classpath = testSourceSets.runtimeClasspath
        testClassesDirs = testSourceSets.output.classesDirs
        useJUnitPlatform()
        javaLauncher.convention(javaToolChains.launcherFor {
          languageVersion.convention(JavaLanguageVersion.of(javaVer))
        })
      }
    }.onEach { pair ->
      tasks.named("check").configure { dependsOn(pair.second) }
    }.fold(mapOf()) { acc, pair ->
      acc + pair
    }
}

extensions.add<ProjectTestConfig>("testConfig", ProjectTestConfig())

なお、このスクリプトですが、書いている途中でコンパイルエラーが発生すると補完が効かなくなるので、気合で乗り越えていく必要があります。 これが僕が Kotlin DSL をあまり好まない理由の一つです。

f:id:mike_neck:20201212194334p:plain

f:id:mike_neck:20201212194356p:plain
tasks くらい取れるだろ

f:id:mike_neck:20201212194446p:plain
`launcherFor` だったっけな? `languageVersion` だったっけな?補完してくれなきゃわからん

最後に、ルートディレクトリーつまりメインプロジェクトの build.gradle.kts です。ここで、先程の .gradle.kts ファイルの拡張子を抜いた部分(ここまでの例だと example.gradle.ktsexample) をプラグインとして指定します。

plugins {
    `example`
}

// テストは Java 11 と 15 でも実行する
testConfig.testOnJavaVersion(11, 15)

かなりシンプル(というより、なにもない)になりました。

では、 Java 8 以外で実行すると落ちるようなテストを書いて実行してみます

class Version {

    @Test
    void show() {
        String javaHome = System.getProperty("java.home");
        assertThat(javaHome).contains("jre");
    }
}

f:id:mike_neck:20201212195301p:plain
check タスクから起動。途中のテストで落ちるので continue オプション付き

f:id:mike_neck:20201212195430p:plain
Java 11 と 15 でめでたくテストが落ちたようです

というわけで、 kotlin-dsl を使うとビルドスクリプトがシンプルになったと思います。また、 groovy での記事でも書いたように複数のサブプロジェクトに対してプロジェクト横断的に同一な記述が必要になるケース(例えば、同じ Kotlin のバージョンと Linter と フォーマッターを使いたい)で、 kotlin-dsl で作ったプラグインをサブプロジェクトで利用できます。


kotlin-dsl のおさらい

  1. プロジェクト(プロダクト)用の build.gradle.kts から命令的なコードを取り除いて、 buildSrc プロジェクトに命令的なコードを記述する
  2. buildSrcプラグインを書く場合に kotlin-dsl を使うと .gradle.kts ファイルを作成する
    1. で作成した .gradle.kts ファイルのピリオドよりも前の部分をメインのスクリプトプラグインに適用する

以上のような形でビルドスクリプトをシンプルにできます。

Kotlin 愛好会で発表した #love_kotlin

f:id:mike_neck:20171113232305p:plain

テストフレームワーク ktcheck の宣伝も兼ねて発表してきた

love-kotlin.connpass.com

資料はこれ

www.slideshare.net


現在勤めている会社のテストのほぼすべては kotlintest(現 kotest) で記述されているのですが、資料にあるようなテストデータ(変数)の状態がうまく扱えないこともあり、どの順序で実行されているかを調べていたりしました。

mike-neck.hatenadiary.com

変数の状態良くわからん問題は発表中の 反応 を見ても共感を得られていたっぽいので、大体つまずく場所は一緒だということも認識できました。

https://twitter.com/search?q=(%23love_kotlin)%20until%3A2020-07-30%20since%3A2020-07-29&src=typed_query&f=live

「これひどいよな」みたいに愚痴を言おうとしましたが、これだけ大きなテストフレームワークを開発できるのだから何か回避策があるに違いないとドキュメントを読み直して IsolationMode のドキュメントにたどり着いてテストを書き直したいと思ったりしました。ドキュメント読み込み大事。


ktcheck はまだ作ったばかりでフィードバックがほしいところなので、ぜひ使ってもらいたいなと思っています|ω・`)チラッ

Kotlin 用のテストフレームワークをリリースしました

表題の通り、テストフレームワークをリリースしました。

名前は ktcheck です。

JUnit Platform 上で動かせますが、普通のメインプログラムの中でも使えます。

利用方法

インストール・導入

gradle の場合、次の記述を dependencies ブロックに記述します。

dependencies {
  testImplementation("run.ktcheck:ktcheck:v0.1.0")
}

テストの記述

object を作って、 KtCheck インターフェースを実装させて、 Given.When.Then で生成されるオブジェクトに移譲します。

object IntPlusTest: KtCheck
by Given("Int の値 1 に", { 1 })
  .When("Int の値 3 を足すと", { int -> int + 3 })
  .Then("4 になる", {_, result -> result shouldBe 4})

条件を Given に記述し、操作を When に記述して、 Thenアサーションを記述するのを強制するようなフレームワークになっています。

あとはテストの実行ですが、 IntelliJ では ktcheck のテストを検出できませんので、 gradle のテストタスクを起動します

テストが実行されて、結果も表示されます。他の gwh スタイルのテストフレームワークよりも明確にテストの名前が表示されるので落ちたテストについてどのようなテストであるかがわかりやすくなっています(kotest のいやなところをこちらは解決した)。

f:id:mike_neck:20200413234557p:plain

もう少し、凝った使い方もできますが、とりあえず省略です。

レポジトリーはこちらです

github.com

あと、ロゴを作ってくれる人いたら紹介してください。