Kotlinでスクリプトを書き始める時に使っているテンプレートについて紹介する

# この記事はなに？

この記事は [Kotlin Advent Calender 2020](https://qiita.com/advent-calendar/2020/kotlin) 14日目の記事です。

以前からKotlin/JVMをサーバサイド開発に使っていますが、最近は簡単なスクリプトを書くときにもKotlinを使うことが増えました（前はよくPythonを使っていましたが、開発チーム的に言語をなるべく揃えた方がよさそうなので…）。
毎回イチからコードを書くのは面倒なので、とりあえず自分の中でストレスなく動かし始められる状態にしたテンプレートをGitHubにおいています。

この記事では、このテンプレートでどんなものを使っているか紹介していきます。

# 環境

* macOS 10.15
* JDK 11
* Kotlin 1.4
* Gradle 6.7.1
* Docker 19
* IntelliJ IDEA Community 2020.1

# テンプレート

ここにある。
https://github.com/lethe2211/kotlin-gradle-sample

必要に応じて随時更新していくつもり。


# やりたかったこと

* できるだけ早く動かせる状態にしたい。毎回必要な依存を考えて入れたくないし、コマンド1つで起動やビルドができる状態にしておきたい
* Kotlinの開発にはIntelliJ IDEAをよく使うので、支障なく動くようにしておきたい
* ソースコードはGit管理したい
* Docker/Kubernetesからすぐに利用できるようにしておきたい
* でも明らかに不要なものは入れたくない。デバッグが難しくなるため

# .gitignore

基本的にスクリプトはGit管理する前提なので、最初に.gitignoreを追加しておく。

```:.gitignore
# Gradle
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar

# IntelliJ IDEA
.idea
*.iws
*.iml
*.ipr
/out/
```

.gitignoreと、実際にどのファイルをGit管理下に置くべきかについては理解が足りていない部分も多いので、必要に応じて管理するファイルを増やしたり減らしたりしている…

ついでに、ここでGit管理を開始しておく。

```bash

$ git init

$ git add .gitignore && git commit -m 'Initial commit'
```

# Gradleの設定
Kotlinでアプリケーションを書くときはビルドツールにGradleを使うことが多い（Mavenは正直あまり使ったことがない）。
ベースはIntelliJ IDEAのGradleテンプレートを使っている。

プロジェクト作成時、以下のように、Gradle Kotlin DSLを有効にしている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/60766191-2c9d-1c23-8bdb-3e5d4358dc83.png)


# build.gradle.kts

最初に、IntelliJ IDEAが生成するテンプレートのGradle Wrapperのバージョンが少し低いので、最新に上げておく（気にならなければ上げなくてもよい）。
この記事を書いた当時の最新バージョンは6.7.1。

```bash
$ gradle wrapper --gradle-version 6.7.1
```

この時点で、 `build.gradle.kts` は以下のようになっている。

```gradle:build.gradle.kts
plugins {
    kotlin("jvm") version "1.4.0"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
}

tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
}
```

これを、

```gradle:build.gradle.kts
plugins {
    kotlin("jvm") version "1.4.0"
    application

    // Shadow Plugin (to generate a Fat JAR)
    id("com.github.johnrengelman.shadow") version "6.1.0"

    // ktlint Plugin
    id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
}

group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))

    // JUnit 5
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")

    // MockK
    testImplementation("io.mockk:mockk:1.10.3")
}

application {
    mainClassName = "org.example.demo.MainKt"
}

tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    test {
        useJUnitPlatform() // Enable JUnit 5 support of Gradle
    }
}

```

こんな感じに変更した。
以下、それぞれの変更内容について見ていくこととする。

# Gradle Application Plugin

Gradle Application Pluginは、JVMアプリケーションのローカル実行やビルドを補助するためのプラグイン。
Javaアプリケーションと組み合わせることも多いが、今回はKotlinと組み合わせて使っている。

https://docs.gradle.org/current/userguide/application_plugin.html

```gradle:build.gradle.kts
plugins {
    application // Gradle Application Plugin
}

application {
    mainClassName = "org.example.demo.MainKt" // Class name run task will execute
}
```

この設定をしたあと、IntelliJ IDEAでGradleのキャッシュを更新すると、 `run` というタスクが新しく生成されていることがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/ae8eaf61-b78d-d706-13dc-9d61d44d2cca.png)

このタスクを実行すると、`mainClassName`に指定したクラスの`main`メソッドが実行される。

Kotlinでは、トップレベルの関数を宣言した場合は、デフォルトでは`ファイル名（拡張子を除く）+Kt`というクラスが作成され、内部的にはそのクラス内で宣言した関数が実行される。

したがって、今回は`src/main/kotlin/org/example/demo/Main.kt`というファイルを作成し、ここにトップレベルの`main`関数を配置することとする。
これにより、内部的には`org.example.demo.MainKt`の`main`メソッドが呼ばれることになる。

```kotlin:src/main/kotlin/org/example/demo/Main.kt
package org.example.demo

fun main(args: Array<String>) {
    println("Hello World")
}
```

IntelliJ IDEAでGradleの`run`タスクを実行すると、`Hello World`が出力されることが確認できる。

Application Pluginは、内部的にGradle Java Pluginと、Gradle Distribution Pluginを含んでいる。
詳しい挙動が知りたい際には、これらのプラグインのドキュメントも参考になる。

Java Plugin: https://docs.gradle.org/current/userguide/java_plugin.html#java_plugin
Distribution Plugin: https://docs.gradle.org/current/userguide/distribution_plugin.html#distribution_plugin

1点、

```gradle:build.gradle.kts
application {
    mainClassName = "org.example.demo.MainKt"
}
```

という記述は、Gradle 6.7ではDeprecatedになっており、IntelliJ IDEA等では、

```gradle:build.gradle.kts
application {
    mainClass.set("org.example.demo.MainKt")
}
```

に変更するように促される。

古い記述を利用している理由は、後述するGradle Shadow Pluginとの互換性の問題のため。
https://github.com/johnrengelman/shadow/issues/609

おそらく今後のShadow Pluginのアップデートで修正されると思われるが、いまはこのままにしておく。

# Fat JARを作るための設定

ここまでの状態でGradleの`clean`タスク、`bulid`タスクをこの順で実行すると、`build/libs/${PROJECT_NAME}-1.0-SNAPSHOT.jar`という名前の実行可能JARファイルが作成される。

ただし、このJARファイルを実行しようとすると、

```bash
$ java -jar build/libs/gradle-kotlin-sample-1.0-SNAPSHOT.jar
no main manifest attribute, in build/libs/gradle-kotlin-sample-1.0-SNAPSHOT.jar
```

というエラーが出て実行できない。
JARのアーカイブ時の設定を見直すことで、生成されるJARファイルがそのまま実行可能なものにする。

まず、この状態のJARを展開してみる（JARファイルはZIP形式でアーカイブされているので、ZIP形式に対応した解凍ツールで解凍することで中身を見ることができる）。

```bash
$ unzip build/libs/gradle-kotlin-sample-1.0-SNAPSHOT.jar -d build/libs/gradle-kotlin-sample/
Archive:  build/libs/gradle-kotlin-sample-1.0-SNAPSHOT.jar
   creating: build/libs/gradle-kotlin-sample/META-INF/
  inflating: build/libs/gradle-kotlin-sample/META-INF/MANIFEST.MF
   creating: build/libs/gradle-kotlin-sample/org/
   creating: build/libs/gradle-kotlin-sample/org/example/
   creating: build/libs/gradle-kotlin-sample/org/example/demo/
  inflating: build/libs/gradle-kotlin-sample/org/example/demo/MainKt.class
  inflating: build/libs/gradle-kotlin-sample/META-INF/gradle-kotlin-sample.kotlin_module

$ cat build/libs/gradle-kotlin-sample/META-INF/MANIFEST.MF
Manifest-Version: 1.0

```

大きく分けて、2点の問題点がある。

1. メインクラスが指定されていない。解凍したJARファイルの内部には`META-INF/MANIFEST.MF`という、JARアーカイブの定義が書かれたファイルがあり、ここに`Main-Class`という名前のエントリを追加することで、JARファイルを`java -jar`コマンドで起動した際のメインクラスを指定できる。
2. クラスパスにある依存クラス（Kotlinの標準ライブラリやサードパーティ製ライブラリなど）がJARアーカイブに含まれていない。何らかの方法でこれらを含める必要がある。

これらの設定を適用して、そのファイル以外に依存を必要とせず実行可能になったJARファイルは、俗にFat JARとかUber JARと呼ばれている。

Fat JARの作り方はいろいろあるが、比較的簡単なのは、Gradle Shadow Pluginを利用する方法。
https://imperceptiblethoughts.com/shadow/

```gradle:build.gradle.kts
plugins {
    id("com.github.johnrengelman.shadow") version "6.1.0"
}
```

プラグインをインストールするだけで、`shadowJar`や`runShadow`（Gradle Application Pluginと一緒にインストールした場合のみ）といった新しいGradleのタスクが生成される。

例えば、IntelliJ IDEAでGradleの`shadowJar`タスクを実行すると`build/libs/`以下に`gradle-kotlin-sample-1.0-SNAPSHOT-all.jar`のようなFat JARが生成される。

```bash
$ ls -l build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar

$ unzip build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar -d build/lib
s/gradle-kotlin-sample/
Archive:  build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar
   creating: build/libs/gradle-kotlin-sample/META-INF/
  inflating: build/libs/gradle-kotlin-sample/META-INF/MANIFEST.MF
   creating: build/libs/gradle-kotlin-sample/org/
   creating: build/libs/gradle-kotlin-sample/org/example/
   creating: build/libs/gradle-kotlin-sample/org/example/demo/
  inflating: build/libs/gradle-kotlin-sample/org/example/demo/MainKt.class
  inflating: build/libs/gradle-kotlin-sample/META-INF/gradle-kotlin-sample.kotlin_module
  inflating: build/libs/gradle-kotlin-sample/META-INF/kotlin-stdlib-jdk8.kotlin_module
   creating: build/libs/gradle-kotlin-sample/kotlin/
  ...

$ cat build/libs/gradle-kotlin-sample/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.example.demo.MainKt

$ java -jar build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar
Hello World
```

プラグインを適用すると、`build`から`shadowJar`へ自動的に依存が張られるので、特に気にならなければ`build`タスクを使うとよい。

JARアーカイブについての詳しい仕様は以下を参照。
https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html

# ktlint（Linter）

KotlinのLinterで有名なものとして、

* ktlint: https://ktlint.github.io/
* detekt: https://detekt.github.io/detekt/ （こちらは正確には静的解析ツールと言った方が正しい？）

があるが、自分がよく使うのはktlint。

こちらもGradleのプラグインがあるのであまり何も考えず組み込んでいる。
https://github.com/JLLeitschuh/ktlint-gradle

```gradle:build.gradle.kts
plugins {
    id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
}
```

このプラグインもインストール時にGradleにいろいろなタスクを生やす。
よく使うのが、

* `ktlintCheck`: 現在のGradleプロジェクト（に含まれているすべてのSource SetとKotlin Script）について、ktlintを使った文法チェックを行う。失敗した場合はエラー終了する
* `ktlintFormat`: 現在のGradleプロジェクト（に含まれているすべてのSource SetとKotlin Script）について、ktlintを使った自動フォーマットを行う。一部自動で修正できないものがあり（ワイルドカードimportなど）、見つけた場合はエラー終了する。Gitの毎コミット前にとりあえずこれを流しておくイメージ
* `ktlintApplyToIdea`: ktlintのルールをIntelliJ IDEAに反映させる。`.idea`を書き換えるので注意

あたり。

これも、`build`タスク（正確には`check`タスク）にktlintの文法チェックタスクへの依存が張られ、文法チェックが失敗するとビルドが落ちるようになるので、CIの用途にも使える。

# JUnit 5

単体テストにはJUnit 5を使っている。

GradleにはネイティブでJUnit 5をサポートする機能があるので、ついでにこれも有効化しておく。
https://docs.gradle.org/current/userguide/java_testing.html#using_junit5

```gradle:build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks {
    test {
        useJUnitPlatform() // Enable JUnit 5 support of Gradle
    }
}
```

例えば、

```kotlin:src/main/kotlin/org/example/demo/sample/Calc.kt
package org.example.demo.sample

class Calc(
    val origNum: Int
) {
    fun add(num: Int): Int {
        return origNum + num
    }
}
```

```kotlin:src/test/kotlin/org/example/demo/sample/CalcTest.kt
package org.example.demo.sample

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

internal class CalcTest {
    lateinit var calc: Calc

    @BeforeEach
    fun setUp() {
        calc = Calc(2)
    }

    @Test
    fun `add can return the summation of origNum and num`() {
        val expected = 6
        assertEquals(expected, calc.add(4))
    }
}
```

こんなコードと単体テストを書いて、Gradleの`test`タスクを実行すると、IntelliJ IDEA上でテスト結果が見られる。
これも、`build`タスクに依存が張られるのでCI用途に使える。

# MockK

単体テストにおいて、テスト対象となるクラスが他のクラスに依存している場合（移譲など）、依存するクラスをモックできると便利な場合がある。

Kotlinのモックライブラリで有名なのは、

* Mockito: https://site.mockito.org/ 
    * MockitoはもともとJava向けのライブラリであり、Kotlinと組み合わせる時はmockito-kotlinというブリッジライブラリも依存に含める必要がある https://github.com/nhaarman/mockito-kotlin
* MockK: https://mockk.io/

がある。

Spring Frameworkを使ったサーバサイド開発をする時はMockitoを使うことが多いが（SpringにはMockitoの組み込みサポートがある）、最近素のKotlinを使う時はMockKを使うようにしている。
理由は、Mockitoに比べてKotlinの文法のサポートがしっかりしており、Mockitoで書くと汚くなる部分を比較的きれいに書けるようになるから。

例えば、

```kotlin:src/main/kotlin/org/example/demo/sample/Calc2.kt
package org.example.demo.sample

class Calc2(
    private val dependency: Dependency
) {
    fun delegate(): Int {
        return dependency.calc(1, 2)
    }
}
```

```kotlin:src/main/kotlin/org/example/demo/sample/Dependency.kt
package org.example.demo.sample

interface Dependency {
    fun calc(a: Int, b: Int): Int
}
```

```kotlin:src/main/kotlin/org/example/demo/sample/DependencyImpl.kt
package org.example.demo.sample

class DependencyImpl : Dependency {
    override fun calc(a: Int, b: Int): Int {
        return a + b
    }
}
```

こんなクラスに対して、

```kotlin:src/test/kotlin/org/example/demo/sample/Calc2Test.kt
package org.example.demo.sample

import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
internal class Calc2Test {
    lateinit var calc2: Calc2

    @MockK
    lateinit var dependency: Dependency

    @BeforeEach
    fun setUp() {
        calc2 = Calc2(dependency)
    }

    @Test
    fun `delegate can return the value of Dependency#calc`() {
        // Mock the behavior
        every { dependency.calc(any(), any()) } returns Int.MAX_VALUE

        val expected = Int.MAX_VALUE

        assertEquals(expected, calc2.delegate())

        // Verify if the mocked method was called
        verify(exactly = 1) { dependency.calc(any(), any()) }
    }

    @Test
    fun `delegate can throw an IllegalStateException when Dependency#calc throws an IllegalStateException`() {
        // Mock the behavior
        every { dependency.calc(any(), any()) } throws IllegalStateException("Unexpected exception.")

        // Assert that an IllegalStateException was really thrown
        assertThrows<IllegalStateException> {
            calc2.delegate()
        }

        // Verify if the mocked method was called
        verify(exactly = 1) { dependency.calc(any(), any()) }
    }
}
```

```kotlin:src/test/kotlin/org/example/demo/sample/DependencyImplTest.kt
package org.example.demo.sample

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

internal class DependencyImplTest {
    lateinit var dependency: Dependency

    @BeforeEach
    fun setUp() {
        dependency = DependencyImpl()
    }

    @Test
    fun `calc can return the summation of a and b`() {
        val expected = 3

        assertEquals(expected, dependency.calc(1, 2))
    }
}
```

こんなテストが書ける。

# Dockerfile

最近は作ったスクリプトをDockerコンテナ化することも多いので、Dockerfileも用意している。

用途に応じてDockerfileの内容を変えている。

自分の手元でしか動かさない場合は、

```dockerfile:Dockerfile
# Note that you need a build before running this Dockerfile

FROM adoptopenjdk:11-jre-hotspot
WORKDIR /opt/app
COPY build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar /opt/app
ENTRYPOINT ["java", "-jar", "/opt/app/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar"]
```

のように、JARファイルのビルドを前提としたDockerfileを置いておき、README等にビルドの手順を記載している。
スクリプトを編集するたびに、JARファイルのビルド -> Dockerイメージのビルドという手順を繰り返す必要があるが、JARファイルのビルドをGradleに任せるとキャッシュがきくので早いという利点がある。

逆に、Docker実行環境が頻繁に変わる場合（他人に配るスクリプトなど）については、

```dockerfile:Dockerfile
# Store Gradle cache so that it won't download the dependencies again and again
# This stage takes a little long time if you run it for the first time
FROM gradle:6.7.1-jdk11 AS cache
WORKDIR /opt/app
ENV GRADLE_USER_HOME /cache
COPY build.gradle.kts gradle.properties settings.gradle.kts ./
RUN gradle --no-daemon build --stacktrace

# Build stage
FROM gradle:6.7.1-jdk11 AS builder
WORKDIR /opt/app
COPY --from=cache /cache /home/gradle/.gradle
COPY . .
RUN gradle --no-daemon build --stacktrace
RUN ls -l /opt/app/build/libs/

# Runtime stage
FROM adoptopenjdk:11-jre-hotspot
WORKDIR /opt/app
COPY --from=builder /opt/app/build/libs/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar /opt/app
ENTRYPOINT ["java", "-jar", "/opt/app/gradle-kotlin-sample-1.0-SNAPSHOT-all.jar"]
```

のように、JARファイルのビルドをDockerfile中のステージに含めたDockerfileを置いている。
この方が手作業でのビルドを前提としない分きれいだが、初回のdocker buildの際にGradleの依存のキャッシュを貯める時間がかかる（数分~数十分）。2回目からはDockerのイメージキャッシュが利用されるので早くなる。

# 感想

* こういうテンプレートを言語ごとに作っておきたい
* ホントはSpring Cloud Busの話をする予定だったが、実装する時間が取れなかった…

# 参考

* [Gradle dependency caching during docker multi stage build?
](https://stackoverflow.com/questions/59007208/gradle-dependency-caching-during-docker-multi-stage-build)
* [Gradle使い方メモ](https://qiita.com/opengl-8080/items/4c1aa85b4737bd362d9e)
* [Gradle Kotlin DSL入門](https://qiita.com/toliner/items/8b1ed6ed3cc04c22d63d)