TL; DR
みなさんGroovyという言語をご存知でしょうか?
一言で言うとJVM上で動くRubyみたいな柔軟な構文の言語です。1
Groovyで開発したことはないと言う人でもJenkinsfileやbuild.gradle等でお世話になっている人はそれなりにいるのではないでしょうか?(私もその一人です)
複雑なJenkinsfileやbuild.gradleを書いているとビルドスクリプトのテストやデバッグが大変になってきます。2
Jenkinsジョブやビルドは一回の実行に数分かかることもよくあるので、printfデバッグでは生産性が非常に悪いです。
実はGroovyはユニットテストも簡単に書けてJavaと同じように実行したりデバッグできるのです!
この記事ではIDE(Eclipse)およびCI(GitLab-CI)でGroovyのテストをどうやって実行するのかを紹介します。
セットアップ
GroovyのビルドツールにはGradleを用います。Gradleの設定ファイルもGroovyで書かれていることからもわかるように親和性が高いです。
Gradleのインストール
以下のリンクを参考にダウンロードします。最新バージョンは7.3.1
インストールが終わったらgradle --version
でバージョンを確認しましょう。
例
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のプロジェクトを作成できます。
例
> 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のプラグインをインストールしましょう。
Help > Eclipse Market Place
から groovy development tools
で検索してインストールします。
プロジェクトのインポート
File > Import
から Gradle > Existing Gradle Project
を選択します。
gradle init
を実行したディレクトリ(lib
ではないので注意)をプロジェクトルートディレクトリに指定します。
他はデフォルトのままで、完了
ボタンを押します。
プロジェクトルートディレクトリと、lib
ディレクトリがそれぞれプロジェクトに表示されていればOKです。
[TroubleShooting] JDKがインストールされていないエラーが出た場合
デフォルトの設定だと JavaSE-17
が指定されます。 EclipseにJava17のJDKがインストールされていない場合、エラーになります。
その場合はJDKをインストールするか、あるいはbuild.gradle
を修正してJavaのバージョンを下げましょう。
例
java {
sourceCompatibility = '11'
targetCompatibility = '11'
}
実行
テストの実行
テストのファイルを選択して、Run > As JUnit Test
(あるいはAlt + Shift + X -> T
)を選択するとテストが実行できます。
デバッグ
次はデバッグ方法を試してみましょう。
LibraryTest.groovy
の以下の行の左側をダブルクリックし、ブレークポイントを設置します。
def result = lib.someLibraryMethod()
今度はDebug > As JUnit Test
から実行してみましょう。
デバッグ画面に切り替わり、ブレークポイントで停止しているはずです。
ステップインして関数の中に入ったり、ステップ実行したりもJavaのデバッグと同じ感覚でできます。
カバレッジ
さらにカバレッジの取得方法も確認しておきましょう。
今度はCoverage > As JUnit Test
から実行してみましょう。
テストが実行され、カバレッジが表示されます。(ダークテーマだと見づらいのでライトテーマに変更してスクリーンショットを取得している)
CIの設定
さて、ローカルでテスト実行とカバレッジが取得できるようになったので、今度はCIでも実行できるように設定しましょう。
Gradleでのカバレッジの取得
build.gradleにjacoco
プラグインを追加して、XMLのレポートを出力できるようにします。
plugins {
id 'groovy'
id 'java-library'
+ id 'jacoco'
}
+jacocoTestReport {
+ reports {
+ xml.required = true
+ html.required = false
+ }
+}
そうすると以下のコマンドでテスト結果とカバレッジが出力できるようになります。
gradlew test jacocoTestReport
出力先は以下の通りです。
- JUnit:
build/test-results/test/TEST-*.xml
- Coverage:
build/reports/jacoco/test/jacocoTestReport.xml
CIでの設定
今回はGitLab-CIでの設定をしてみました。
詳しい設定方法はこの辺のドキュメントを読むとわかります。
以下の内容を.gitlab-ci.yml
に保存してレポジトリにコミットするとCIの設定ができます。
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
でカバレッジの結果を読み込んでいます。
-
試しに、メソッドとテストを追加してみましょう。
boolean someLibraryMethod() {
true
}
+int max(int a, int b) {
+ if( a < b ) {
+ return b;
+ } else {
+ return a;
+ }
+}
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
+ }
}
マージリクエストを作成すると、テスト結果が表示されます。
マージリクエストのdiffタブを開くとカバレッジが表示されます。
then節はカバレッジが通っているがelse節はカバレッジが通っていないことがわかります。
終わりに
今回紹介したように、実はGroovyでも気軽にユニットテストを動かすことができます。
GroovyのテストフレームワークSpockは表現力が高く、
Javaのプロジェクトの単体テストをGroovyで書いてみると言うのもありかもしれません。