Groovyでもユニットテストを書こう

# TL; DR
みなさんGroovyという言語をご存知でしょうか？
一言で言うとJVM上で動くRubyみたいな柔軟な構文の言語です。[^1]

http://groovy-lang.org/

[^1]: https://ja.wikipedia.org/wiki/Groovy#%E4%BB%96%E3%81%AE%E8%A8%80%E8%AA%9E%E3%81%8B%E3%82%89%E3%81%AE%E5%BD%B1%E9%9F%BF

Groovyで開発したことはないと言う人でもJenkinsfileやbuild.gradle等でお世話になっている人はそれなりにいるのではないでしょうか？（私もその一人です）

複雑なJenkinsfileやbuild.gradleを書いているとビルドスクリプトのテストやデバッグが大変になってきます。[^2]
Jenkinsジョブやビルドは一回の実行に数分かかることもよくあるので、printfデバッグでは生産性が非常に悪いです。

[^2]: そもそも複雑なものを書くべきではない

実はGroovyはユニットテストも簡単に書けてJavaと同じように実行したりデバッグできるのです！

この記事ではIDE(Eclipse)およびCI(GitLab-CI)でGroovyのテストをどうやって実行するのかを紹介します。

# セットアップ
GroovyのビルドツールにはGradleを用います。Gradleの設定ファイルもGroovyで書かれていることからもわかるように親和性が高いです。


## Gradleのインストール

以下のリンクを参考にダウンロードします。最新バージョンは7.3.1

https://gradle.org/install/

インストールが終わったら`gradle --version`でバージョンを確認しましょう。

### 例

```console
gradle --version
------------------------------------------------------------
Gradle 7.3.1
------------------------------------------------------------
Build time:   2021-12-01 15:42:20 UTC
Revision:     2c62cec93e0b15a7d2cd68746f3348796d6d42bd
Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          1.8.0_282 (Amazon.com Inc. 25.282-b08)
OS:           Windows 10 10.0 amd64
```

## Projectの初期化

`gradle init`でインタラクティブにgroovyのプロジェクトを作成できます。

### 例

```console
> gradle init
Starting a Gradle Daemon (subsequent builds will be faster)
Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 3
Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 2
Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1
Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]                                                                                                                       yes
Project name (default: groovy-example): groovy-example
Source package (default: groovy.example): example
> Task :init
Get more help with your project: https://docs.gradle.org/7.3.1/samples/sample_building_groovy_libraries.html
BUILD SUCCESSFUL in 1m 27s
2 actionable tasks: 2 executed
```

## ディレクトリ構成の確認

さて、一旦ディレクトリ構成を確認しておきましょう。

```
.
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib
│   ├── build.gradle
│   └── src
│       ├── main
│       │   ├── groovy    # メインソースディレクトリ
│       │   └── resources # メインリソースディレクトリ
│       └── test
│           ├── groovy    # テストソースディレクトリ
│           └── resources # テストリソースディレクトリ
└── settings.gradle
```

multi-projectのディレクトリ構成となっています。
`lib`以下が実際のプロジェクトで、`src/main/groovy`に実際のコードを置いて行きます。

ディレクトリ構成が気に入らない場合は適宜修正して`build.gradle`も修正しましょう。

# IDEの設定
GroovyをサポートしているIDEはIntelliJ IDEAとEclipseです。
（VSCodeの良いプラグインがあったら教えてください）

今回はEclipse（21-12)での手順を紹介します。

## プラグインのインストール
まずは、以下のGroovyのプラグインをインストールしましょう。

https://marketplace.eclipse.org/content/groovy-development-tools

`Help > Eclipse Market Place`から `groovy development tools`で検索してインストールします。


## プロジェクトのインポート

`File > Import`から `Gradle > Existing Gradle Project`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/5e730d2a-b1f0-0cb8-a348-1e859b95795a.png)

`gradle init`を実行したディレクトリ（`lib`ではないので注意）をプロジェクトルートディレクトリに指定します。

他はデフォルトのままで、`完了`ボタンを押します。

プロジェクトルートディレクトリと、`lib`ディレクトリがそれぞれプロジェクトに表示されていればOKです。
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/69eac845-ae27-97e4-be57-fd7a304aef93.png)

## [TroubleShooting] JDKがインストールされていないエラーが出た場合
デフォルトの設定だと `JavaSE-17`が指定されます。　EclipseにJava17のJDKがインストールされていない場合、エラーになります。

その場合はJDKをインストールするか、あるいは`build.gradle`を修正してJavaのバージョンを下げましょう。

### 例
```groovy:build.gradle
java {
	sourceCompatibility = '11'
	targetCompatibility = '11'
}
```

# 実行

## テストの実行
テストのファイルを選択して、`Run > As JUnit Test`（あるいは`Alt + Shift + X -> T`)を選択するとテストが実行できます。

![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/f735aba5-b5fb-f291-168c-16f436066676.png)


## デバッグ
次はデバッグ方法を試してみましょう。

`LibraryTest.groovy`の以下の行の左側をダブルクリックし、ブレークポイントを設置します。


```
def result = lib.someLibraryMethod()
```

今度は`Debug > As JUnit Test`から実行してみましょう。

デバッグ画面に切り替わり、ブレークポイントで停止しているはずです。

![image (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/7f617929-4f01-49f9-7a44-e9d3cab5740e.png)

ステップインして関数の中に入ったり、ステップ実行したりもJavaのデバッグと同じ感覚でできます。

## カバレッジ
さらにカバレッジの取得方法も確認しておきましょう。

今度は`Coverage > As JUnit Test`から実行してみましょう。

テストが実行され、カバレッジが表示されます。（ダークテーマだと見づらいのでライトテーマに変更してスクリーンショットを取得している）

![image (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/545610bc-b708-f43b-bf06-3314bcd708ce.png)


# CIの設定

さて、ローカルでテスト実行とカバレッジが取得できるようになったので、今度はCIでも実行できるように設定しましょう。

## Gradleでのカバレッジの取得

build.gradleに`jacoco`プラグインを追加して、XMLのレポートを出力できるようにします。

```diff_gradle:build.gradle
 plugins {
   id 'groovy'
   id 'java-library'

+  id 'jacoco'
 }

+jacocoTestReport {
+    reports {
+        xml.required = true
+        html.required = false
+    }
+}
```

そうすると以下のコマンドでテスト結果とカバレッジが出力できるようになります。

```console
gradlew test jacocoTestReport
```

出力先は以下の通りです。

- JUnit: `build/test-results/test/TEST-*.xml`
- Coverage: `build/reports/jacoco/test/jacocoTestReport.xml`

## CIでの設定
今回はGitLab-CIでの設定をしてみました。
詳しい設定方法はこの辺のドキュメントを読むとわかります。

https://gitlab-docs.creationline.com/ee/ci/junit_test_reports.html

https://docs.gitlab.com/ee/user/project/merge_requests/test_coverage_visualization.html

以下の内容を`.gitlab-ci.yml`に保存してレポジトリにコミットするとCIの設定ができます。

```yaml:.gitlab-ci.yml
groovy-ci:
  image: gradle:7.3.1-jdk17
  script:
    - gradle -i test jacocoTestReport
  variables: # デフォルトだと`$HOME/.gradle`が使われてキャッシュできないのでプロジェクトローカルに変更
    GRADLE_USER_HOME: $CI_PROJECT_DIR/.gradle_home
  cache:　# 依存関係のキャッシュ
    key:
      files:
        - lib/build.gradle
    paths:
      - .gradle_home/caches
  needs: [] # GitLab 14.2以降ではstageを省略できます
  artifacts:
    when: always
    reports:
      junit: lib/build/test-results/test/TEST-*.xml
    paths:
      - lib/build/reports/jacoco/test/jacocoTestReport.xml
  rules: # マージリクエストとデフォルトブランチでジョブを実行
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_MERGE_REQUEST_IID
 
report-coverage:
  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.7
  script:
    - python /opt/cover2cover.py lib/build/reports/jacoco/test/jacocoTestReport.xml $CI_PROJECT_DIR/lib/src/main/groovy > coverage.xml
  needs:
    - groovy-ci
  artifacts:
    reports:
      cobertura: coverage.xml
  rules:
    - if: $CI_MERGE_REQUEST_IID
```

書き慣れていないと面食らいますが、二つのジョブで行います。

- `groovy-ci`: gradleでのテストの実行とカバレッジの取得を行います。
  - `artifacts:reports`でJUnitの結果を読み込んでいます。
- `report-coverage`: jacoco形式のレポートをGitLabが認識できるcobertura形式に変換します。　
  - `artifacts:reports`でカバレッジの結果を読み込んでいます。


試しに、メソッドとテストを追加してみましょう。

```diff_groovy:Library.groovy
 boolean someLibraryMethod() {
     true
 }

+int max(int a, int b) {
+    if( a < b ) {
+        return b;
+    } else {
+        return a;
+    }
+}
```

```diff_groovy:LibraryTest.groovy
 class LibraryTest extends Specification {
 ...
+    def "max 1 2 returns 2"() {
+        setup:
+        def lib = new Library()
+
+        when:
+        def result = lib.max(1, 2)
+
+        then:
+        result == 2
+    }
 }
```

マージリクエストを作成すると、テスト結果が表示されます。

![image (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/315582c5-dae9-92ab-00d5-f86249c5ce24.png)

マージリクエストのdiffタブを開くとカバレッジが表示されます。

then節はカバレッジが通っているがelse節はカバレッジが通っていないことがわかります。

![image (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/0741fc3b-8027-2043-bf75-2c9d66816a29.png)

# 終わりに

今回紹介したように、実はGroovyでも気軽にユニットテストを動かすことができます。
GroovyのテストフレームワークSpockは表現力が高く、
Javaのプロジェクトの単体テストをGroovyで書いてみると言うのもありかもしれません。

https://qiita.com/umeki_ryo/items/98336bb8badca6dc11ac