White Box技術部

WEB開発のあれこれ(と何か)

JOOQをGradleプロジェクトで使う

2017/12/15追記:gradle buildからJOOQのコード生成を除外

サーバサイドの開発をKotlinで行うために、ここのところ調査・検討を色々していました。

概ね問題なさそうという結論が出たので、それ以外の技術選定をしていたのですが、 O/R Mapperに採用することにしたJOOQをGradleと組み合わせたときに、悩むことが多かったので、 現時点でのやり方と考えを残しておこうと思います。

JOOQ

JOOQ(juke:ジュークと読むらしい)はJava用のO/R Mapperなのですが、KotlinのDSLもあり、Kotlinからでも(私としては)違和感なく使えるライブラリになっています。

JOOQ選定理由の大きなところは以下になります。

  • Kotlin対応が考慮されている
  • DSLSQLの文法とほぼ同じで、JOIN句が書きやすく、直感的になっている
  • ライブラリを利用するための事前準備が、ソースコードと独立している(アノテーション利用ではないこと)
  • 必要であればSQLがそのまま実行できる
  • 公式ドキュメントでそれなりに疑問が解消できる

利用よりも運用が悩ましい

使う分には違和感がなかったJOOQでも、運用方法についてはまだしっくりきていません。

今のところは「Gradleプロジェクトで利用するのであれば、Gradleで完結させよう」という方針で使っています。

Gradleとの組み合わせ

GradleでJOOQを利用するのには、gradle-jooq-pluginを使いました。

以下はkotlin-api-sampleで最初のあたりに書いていた、build.gradleのJOOQ設定部分です。

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.main) {
        jdbc {
            driver = 'org.h2.Driver'
            url = 'jdbc:h2:./test;AUTO_SERVER=TRUE'
            user = 'sa'
            password = ''
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.h2.H2Database'
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
                directory = 'src/main/java'
            }
        }
    }
}

ちなみにdependenciesに記載する、JOOQに関係するruntimeとjooqRuntimeには 以下のようにバージョン番号まで指定しないとgenerateコマンドがエラーとなり、動作しませんでした。

runtime('com.h2database:h2:1.4.196')
jooqRuntime('com.h2database:h2:1.4.196')

この設定を記載した際は以下のことを考えていました。

  • Spring starterで入るJOOQとコード生成に使うJOOQのバージョンを合わせるため、バージョンを指定しよう
  • テーブルに対しての処理だから、タスク名部分はtablesがいいだろう
  • 生成ファイルは直接src/main/javaに出力すれば、管理が楽だろう
  • 生成先のパッケージ名の最後はわかりやすくjooqにしよう
  • 本番環境でコード生成する必要はない(DBマイグレーション管理しているため、開発環境での生成でOKだろう)

実際、これで動作するようには作れるのですが、次第に以下が気になってきます。

  • 実装に利用しないDBスキーマも自動生成対象にしてしまっている
  • gradle cleanすると、src/main/java配下の生成したファイルが消えてしまう

自動生成対象の選定

対象のDBスキーマを一つに絞るのは、inputSchemaを指定することで簡単にできます。
例えば、target_db_nameというMySQLのDBスキーマを対象にした場合は、以下のようになります。

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.main) {
        jdbc {
            driver = 'com.mysql.cj.jdbc.Driver'
            url = 'jdbc:mysql://localhost:3306'
            user = 'admin'
            password = 'admin'
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.mysql.MySQLDatabase'
                inputSchema = 'target_db_name'
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
                directory = 'src/main/java'
            }
        }
    }
}

一つの場合はこれでいいのですが、複数ある場合はこれだと対応ができません。

なので複数の場合はinputSchemaを指定するのではなく、 以下のように不要なものを除く書き方をすることで、必要な分だけをコードにすることができます。

database {
    name = 'org.jooq.util.mysql.MySQLDatabase'
    includes = '.*'
    excludes = '(?i:(information_schema|sys|mysql|performance_schema)\\..*)'
}

試してはいませんが、反対に必要なものだけをincludesに記載するやり方でも同じ結果になるはずです。

gradle cleanから自動生成したコードをガード

これは自動生成コードの出力先をデフォルトに戻し、別タスクでsrc/main/java配下にコピーすることで対応しました。

import org.gradle.util.GFileUtils

中略

sourceSets {
    initJooq
}

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.initJooq) {
        jdbc {
            driver = 'org.h2.Driver'
            url = 'jdbc:h2:./test;AUTO_SERVER=TRUE'
            user = 'sa'
            password = ''
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.h2.H2Database'
                inputSchema = ''
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
            }
        }
    }
}

// cleanタスクで生成されたコードが削除されないようにするため、1つ段階を踏む
task copyJooq(dependsOn: [clean, generateTablesJooqSchemaSource]) {
    doLast {
        File targetDir = new File("src/main/java/box/white/seriwb/api/jooq")
        targetDir.deleteDir()

        File jooqDir = new File("build/generated-src/jooq/tables")
        GFileUtils.copyDirectory(jooqDir, new File("src/main/java"))
    }
}

コピーするタスクを追加するだけでは、自動生成のコードがsourceSets.mainになるため、 クラス重複エラーがコンパイル時に発生するようになるため、sourceSetsに新しくinitJooqを追加し、 生成したコードはこちらのsourceSetになるようにして、クラス重複を回避しています(裏技みたいですね・・・)。

こうして、src/main/javaへの反映をプラグインの仕組みから切り離すことで、 cleanで自動生成ファイルが削除されることと、不意のgenerateTablesJooqSchemaSourceタスク実行によるコード変更はなくなりますが、 コード反映する場合はgradle copyJooqを手動で実行しなければいけないという制約もできます。

ちなみに、この問題の解決策として、『Gradle実行時に生成されることを期待して、自動生成ファイルをコード管理に登録しない』という方法もあると思いますが、 通常はDBスキーマ情報が同一のリポジトリで管理されていないはずですので、1リポジトリ内でコンパイルを成功させるためにも、生成したコードもリポジトリに登録するのが良いと思います。

import文の記載やdoLastの記載は、gradle実行時の警告回避によるものです。 Gradle 5.0.0以降ではtasks定義時の<<演算子などは削除される予定という旨のメッセージが出力されます。

gradle buildからJOOQのコード生成を除外

copyJooqタスクでコード生成を行うため、buildタスクで実行されるgenerateTablesJooqSchemaSourceは意味がなくなりました。 なのでbuild時にgenerateTablesJooqSchemaSourceが実行されないようにしておきましょう。

タスクの除外は、buildコマンドを実行する際にxオプションでgenerateTablesJooqSchemaSourceを指定してもいいのですが、 以下のように『generateTablesJooqSchemaSourceはbuildタスク以外のときに実行する』とbuild.gradleに記載しておくのがいいでしょう。

jooq {
    省略
}
generateTablesJooqSchemaSource.onlyIf { name != 'build' }

以下にこれらの内容が反映されたプロジェクトがありますので、詳細な内容はこちらからご確認ください。

やっぱり

利用までに一手間が入るライブラリは、実際に利用するよりも、運用のほうが難しく感じます。

今回は前提に「Gradleから利用」を置いていたのでこのような結論になったのですが、 もしその前提を外すならば、DBマイグレーションと合わせてタスク化するなど、別の方法も取れると認識していますし、 そちらの方がいいこともあると思っています。

なのでこんな方法で運用しているよーとかあれば、コメントなどで頂けると嬉しいです。

【まとめ】Java SE 9/EE 8リリースイベント 兼 JavaOne 2017 報告会

Javaな仕事に戻ったのもあり、JJUGのイベントに行ってきました。

当日の発表内容や様子は、以下にまとめてくださった方がいるようです。

ちなみにこのイベントの内容は、動画で公開されています。

参加できなかった方もこちらを見ていただけると、Javaの今がわかって良いのではないでしょうか!

といったところで、あとは各発表の私のメモになります。


1. JavaOne 2017 Overview & Announcements

https://youtu.be/XT2tIh9r6Eo?t=0m4s

  • 7の時代はCoin, Lambda, Jigsawを企画していた
  • 9でようやくMilling CoinとJigsawが入った
  • 機能追加が9では91(8のときは55

今まで

  • OpenJDKはOracle JDKと技術的な差がある
  • 2年に1度のリリース目標
    • 細かい機能リリースも合わせてリリースが遅れてしまっていた
  • 長期サポートや更新リリースがあった

これから

  • OpenJDKとOracle JDKの技術的差分を2018年後半までになくす
  • 機能リリースを6ヶ月に1度に
    • バージョン表記は$YEAR.$MONTHになる(JDK 9、18.3、18.9
  • OpenJDKのバイナリは無償配布(OracleJDKは有償になる

スケジュール

  • この1年で移行期間が終わる
  • 後継バージョンのリリースまでがサポート期間になる

2. JDK9, Release Cadence & Future

https://youtu.be/XT2tIh9r6Eo?t=30m24s

資料

ローカル変数の型推論であるAmberが入ると、Javaもだいぶ省力化できるようになりそう

JShellとライブラリの話

発表者が途中で変わる(吉田真也さん @shinyafox @bitter_fox

  • REPLツールが有ると教育分野で人気(これがJShellをつくるモチベーション
  • 新しいコレクションファクトリのSet.of()とかで作ったSetはイミュータブルになる
  • StreamAPIでtakeWhileで途中中断とかできるようになった

3. Intel's Persistent Memory

https://youtu.be/XT2tIh9r6Eo?t=1h18m18s

  • 今までのメモリより容量がめっちゃ増えた(4倍
  • めっちゃ安い(2分の1
  • OracleDB 18cがこれをサポートする
    • 5倍くらい早くなる

Javaでも効果が出るように進めている

PMの使い方

  • メモリのスケールアップ(揮発するメモリ
    • Javaのヒープをのっけてつかう
      • 開発者としては楽ちん
      • ビックデータとかインメモリのアプリでこれを使うといいよ
      • 実行するときのオプションでできるようにしていきたいという話@インテル
        • 1,2年でJavaに入りそうな雰囲気
    • 一部をDRAMに、それ以外をPMに
      • ユーザが割当を指定 or プロファイリング結果をもとに自動割当とか
      • 低頻度アクセスとかサイズの大きいのとかをPMに載せるといい
      • -Xmpオプションとかで指定できるようにしたい
  • ストレージのスケールアップ

自分が定義したクラスをPMに保存するって言う処理も書ける

  • PersistentObjectを継承すると行ける
  • 実装方法はPanamaのクラス定義の仕方を参考にしている

リアルタイムに値を変更できるようになる

  • IntelがめっちゃJavaに力を入れているらしい
  • ストレージのAPIはPanama依存が多いのでPanama待ちかもしれない
  • 対応するOSS DBが出てくるのはまだ先になりそう

4. Java EE 8

https://youtu.be/XT2tIh9r6Eo?t=2h25s

現段階で話せることが今回の発表内容

  • Java EE 8
  • EE4J
  • Panel Discussions
  • Key Takeaway

Java EE 8のリリース

JavaEEの開発がGitHubに移管された!

Javaの目指すところ

Open Evolving Nimble Scalable

Java EEは?

Available Open Nimble

もっとOpenに、もっとNimbleにしていかないといけない
->だからEclipse Foundationに移管することに決めた

移管プロジェクトがEE4J(ブランド名はまだ未定

EE4J

https://projects.eclipse.org/projects/ee4j/charter

  • Open
    • プロセスを透明化する
    • 沢山の人に参加してもらって決めていくことを目的にしている
      • Oracleも1ベンダーとしてコミットしていく
  • Compatible
    • 互換性を持たせて、新しい機能を提供していきたい
    • ライセンスをEclipse側にリライセンスする
      • 資産自体はOracleが幇助する
      • 今まであったものはそのまま使えるようにする
      • 新規に作るものはEclipseの流儀に合わせて作っていく
  • Flexible
  • Nimble
    • 2018年の中旬までには移管したい

Java EEOracleはどうサポートしていくのか

  • 今までの契約は引き続きサポートしていく
  • ライセンス許諾したベンダには、2025年までにEE4Jに移行するようにしてくれとアナウンスしている
    • EE4Jの発表を待ってくれという状況

パネルディスカッションの話

MicroProfileが将来的にはJava EEにマージされようとしている

MicroProfileとはなんぞや

Q. 標準化団体どうすんの?(今はJCP

  • 今はできれば作りたくない。時間がかかってしまう懸念がある
  • JSRは?
    • やり方は真似したい(踏襲する流れになっている

MicroProfile 2.0でJava EE 8の機能が入ってくる(2018/3/31

Key takeaway

  • Java EE 8がようやくリリースされた
  • EE4Jは始まったばかり
  • みんな意見を言ってくれ(食い気味に

5. Microservices Topic & Approach

https://youtu.be/XT2tIh9r6Eo?t=2h39m

  • ホテルを早めに予約しないと辛い(10月のサンフランシスコはいろんなイベントがやっている
    • 近くて4万、車で10分15分で2万(1泊
  • 土曜日が40時間ぐらいになる(時差の影響
  • 10月は結構寒い(特に夜
    • 行き先や料金とかもアプリでその場で決定できてよかった(Uber
  • セッションの事前予約はしっかりね
  • パネルディスカッション系のセッションは辛い(英語力ぅ・・・ですかねぇ・・・

セッションの紹介

モノリスからMicroservicesへ

自社の事例を話していたセッションで共通している内容

  • まだ旅は続いている

技術よりも組織・文化についてかなり強調されている

  • コンウェイの法則
    • 組織のコミュニケーション構造がシステムに反映される
    • 逆法則もある
  • Autonomyを何より重視
    • 独立リリースを継続的に可能な状態にする
    • 破壊的な変更はちゃんと手続きを取る
      • エンドポイントの移行期間を設けるとか

今の議論の対象は、サービスという部品をどう協調動作させるかということになっている

Eventual Consistency

結果整合性、いつか一致していればいいというものであれば、マイクロサービスにあっている

Event sourcing

  • データストアをイベント記録に使う
    • データをどんどんインサートしていく(更新しない

CQRS

都合のいいデータストアにEventual Consistencyでデータを同期していくとイイね!

CQRSってなんぞ?

複数ServiceにまたがったTransaction

  • Long Running Action
    • MicroProfileの仕様
  • WildFlyが一部だが単独で利用できる

考察

  • 今、自分たちにマイクロサービスの必要性があるかを考えたほうがいい
  • ある程度は固まっていているほうがいい
  • 組織や文化とセットで考える必要がある
    • 継続的に分散されたサービスを扱っていけないのであれば、きつい
  • どこまでの複雑性や変化を受け入れられるかを考えながら分割するのがいい
    • Service間Transactionとかやりだすとつらそう

6. Fn Project

https://youtu.be/XT2tIh9r6Eo?t=3h31m55s

Functionとは? 入力を受けて何かして出力する小さいプログラム

Functionの問題点 - 各ベンダの独自実装 - Javaのサポートが甘い

理想的なFunctionのプラットフォームとは? - Open Sourceでベンダーロックインされていない - 場所を選ばず使える(クラウド - 簡単に使える - Dockerで動く - デプロイとかで既存のものが使える

Fn projectはApache 2.0 License

FDKというJava用の開発キットが用意されている

CIを利用していてもCIの結果が戻ってくるまで数分かかる Fnではここが数秒間になるようにしようとしている

Fn Flow

複数の処理をまとめられる(コンカレントAPIっぽく使える

ブログ

7. Community&総括

https://youtu.be/XT2tIh9r6Eo?t=3h43m30s

図を出すと反応が多い

JJUG CCC fallの申し込みよろしく

8. LT

懇親会は出てないのですが、見つけた資料の内容がよかったのでこちらも貼っておきます。

所感

  • Javaが生まれ変わろうとしている感じがある
  • Oracle依存が強いとライセンス費用がやばくなりそう
  • 自動生成系のライブラリはこれから辛そう
    • 実際、JOOQの自動生成処理がJava9だとエラーになって動作しない
    • JOOQはまだメンテナスされているからいいにしろ、メジャーバージョンアップデートのときになんとか対応できていた系のライブラリはそのうち使えなくなりそう
  • コンウェイの法則はありますあります
  • マイクロサービスが目的になっているところもありますあります
  • 去年ぐらいにCQRSなデータストア採用できないか考えてたけど、やっぱりまた考えないといけないかなぁ
  • Fn Projectは実際のコード見てみたらなんか期待を感じた

GitHub Enterprise上のRubyコードのカバレッジをCircleCI Enterpriseで集計し、結果をSlack通知する

カバレッジのSlack通知

こちらの内容を参考に、CircleCI Enterpriseでビルド時に集計したカバレッジを、Slack通知できるようにしました。

参考元のスクリプトではcoberturaとjacocoに対応していましたが、simplecovにも対応してあります。 あと、取得元をGitHub Enterpriseでも大丈夫なようにしてあります。

設定手順(SimpleCov利用の場合)

RubyプロジェクトでSimpleCovを使い、カバレッジを取得している場合の設定手順です。

1. Artifactsにカバレッジを保存

circle.xmlでtest実行後にカバレッジのあるcoverageディレクトリをArtifactsに移動(またはコピー)する。

test:
  post:
  - sudo mv coverage $CIRCLE_ARTIFACTS

2. CircleCIのプロジェクト設定

カバレッジ通知対象のCircleCIプロジェクトの設定画面を開き、以下の設定を行う。

API Permissions

PERMISSIONS > API Permissions で Create TokenボタンからBuild Artifactsのトークンを作成する(ラベル名は適当で良い)

Environment Variables

BUILD SETTINGS > Environment Variables で 以下の変数を設定する

Name Value
SLACK_ENDPOINT SlackのWebhook URL
CIRCLE_TOKEN 手順1で作成したトークン値
Test Commands

TEST COMMANDS > Test Commands の Post-test commands にGistに登録したシェルのパスを以下のように設定する

curl -s -L https://gist.github.com/seriwb/31d47a36a108183b1b5c33ae1f51b681/raw/circleci-coverage-slack.sh | bash -s
シェルのサンプル

実際には以下のシェルをコピーなどして、必要なURLを設定して利用して下さい。

3. ビルドする

対象プロジェクトがビルドされれば、Slackにカバレッジ結果が通知されます。

補足

  • GitHubとCircleCIのURLをスクリプト上にベタ書きしているのは、CircleCIでの変数設定を省略するためです
  • 参考元のコードはデフォルトがcoberturaになっていますが、simplecovに変更しています
  • 他のカバレッジを追加する場合は、calcRate関数にそれ用の処理を追加すればOKです
  • simplecovに関しては、CCIEにログインしていればカバレッジのHTMLに直飛び出来るreportリンクを追加しています

Circle Checker作りで学んだこと(GroovyとかSpockでのモックの書き方とか)

技術的なこと

前回はCircle Checkerの紹介だったので、今回は作って学んだことを書こうと思います。

※Circle Checkerの紹介記事はこちらです。

実装の話

まずは、

「1時間で作る!」

とか言って始めたのに、プロジェクトの準備に30分かかっていた件ですが、 ローカルのgitリポジトリGitHubリポジトリを同期するのにコマンドを忘れ、時間がかかったためでした。

GitHub上のLICENSEファイルを使いたかったんです

リモートのGitリポジトリとローカルをマージ

以下が手順のコマンド。

git remote add origin git@github.com:seriwb/circle-checker.git
git fetch origin
git merge origin/master

ただ単に、マージの引数が足りていないだけでした。

便利なDTOの定義方法

Javaではプログラム間でやり取りするデータを扱うクラスをDTOと呼んだりしますが、 DTOは適切なコンストラクタやアクセッサメソッド、equals()、hashCode()の実装がほぼ必須になります。

Groovyではフィールド定義するだけでアクセッサメソッドは用意されますが、他は自前で用意する必要があります。

ですが、@EqualsAndHashCodeアノテーションを付けると、equals()とhashCode()の実装が行われ、 @TupleConstructorアノテーションを付けると、クラスフィールドの数に応じたパラメータを持つコンストラクタが利用できるようになります。

それらを利用してすると、以下のような記述だけでDTOクラスが実装できます。

import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@EqualsAndHashCode
@TupleConstructor
class CircleInfo {

    String twitterName
    String twitterId
    String twitterUrl
}

Twitter APIのカーソリング方法

Twitter APIからカーソル付きの結果を得るときは最初に-1を投げること、とTwitterAPIドキュメントに記載があるので、 Twitter APIのラッパーであるTwitter4jの当該メソッドを利用する場合も、-1相当の値(long型なので-1L)を設定して呼び出す必要があります。

Circle Checkerで該当する実装箇所は、以下などです。

https://github.com/seriwb/circle-checker/blob/master/src/main/groovy/box/white/cc/CircleChecker.groovy#L156

クロージャ内での正規表現チェック

クロージャに渡った値をそのままクロージャ内の正規表現の一部に利用する場合は、 ダブルクォートで囲まれた文字列のときと同様に、$を先頭につけるだけで使えました。

def checkStr = "ああああiiiiうううう"

def filterList = ["aaaa", "iiii", "uuuu"]
filterList.each {
    if (checkStr =~ /.*$it.*/) {
        println(it)
        return
    }
}

※ each内でのreturnはeachを抜けるだけで、後続処理は継続されます

AccessTokenのクリアの仕方

AccessTokenオブジェクトのtokenとtokenSecretをnullに設定したもAccessTokenの設定がクリアできるわけではなく、以下の例外は発生します。

java.lang.IllegalStateException: Access token already available.

Twitter4jでAccessTokenをクリアするには、Twitter#setOAuthAccessTokenでnullをセットするのが正しい方法になります。

落ち穂

私がGroovyなのに型をわざわざ宣言しているのは、見返したときに何に利用しようとしていたかを確認するためと、IDEの補完を有効にするためです。

最初はdefで宣言してざっくりコードを書いて、後々型宣言に修正するという書き方を良くしています。

テスト(Spock)の話

過去にJUnitの記事を書いたように、業務ではJUnitでテストを書くことが多かったのですが、

個人的にJava/GroovyのテストフレームワークSpockに期待を寄せています。

洋書のJava Testing with Spock買って読んだりたりしていたわりには、 今まで実用レベルのテストをSpockで書いていなかったので、今回はMock/Spyの両機能を使ったテストコードを書いてみました。

Gradleの設定

GradleプロジェクトでSpockを利用するために、以下をbuild.gradleの依存関係に追加しますが、 Spockでテストを書くだけであれば、spock-coreだけで大丈夫です。 クラスをモックしたコードを書く場合は、cglibとobjenesisも必要になります。

testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
testCompile group: 'cglib', name: 'cglib-nodep', version: '3.2.5'
testCompile group: 'org.objenesis', name: 'objenesis', version: '2.5.1'

モックとスタブ

モッククラスを作るのはとても簡単で、以下の2通りで宣言できます。
(モック対象のクラスをUserとして、モッククラスuserを宣言する場合)

  • def user = Mock(User)
  • User user = Mock()

モッククラスのメソッドにスタブさせる場合は、当該メソッドに対し、>>演算子を使って返却値を定義してあげるだけです。

user.getName() >> "ダミー名"

実際にモッククラスを使ってテストを実行している例は、以下になります。

   def "フィルタにある名前の結果のみが返却されることを確認"() {
     setup:
        List<String> filterList = ["ああ","uu"]

        def user = Mock(User)
        user.getName() >> name
        user.getScreenName() >> screenName
        user.getURL() >> url

        def file = '''
cc.target.list = "listname"
cc.html.dir = "./dir"
cc.tweet.maxcount = 100
cc.loop.waittime = 600
'''
        def config = new ConfigSlurper().parse(file)

        def testSuite = new CircleChecker(config)
        testSuite.filterList = filterList

     expect:
        testSuite.checkUserName(user) == expected

     where:
        name | screenName | url               | expected
        "ああああ" | "aaaa" | "http://aaaa.com" | new CircleInfo("ああああ", "aaaa", "http://aaaa.com")
        "いいいい" | "iiii" | "http://iiii.com" | new CircleInfo()
        "aaiiuu" | "uuuu" | null              | new CircleInfo("aaiiuu", "uuuu", "")
    }

setupでテスト実施までの処理を記載し、expectにはテストで確認したい比較条件のみを記載します。 また、複数のパラメータでテストを流すために、whereにそれぞれ動的に変更するパラメータを定義することができます。 記述順的に直感的ではないですが、whereで宣言しているパラメータはsetupやexpectでも使えます。

抽象メソッドの実装

後テストでよく必要になるのは、抽象メソッドを持つクラスのインスタンス化ではないでしょうか。

これはGroovyで実現できます。

例えば、Twitter4jのRateLimitStatusクラスに存在するgetRemaining、getLimit、getResetTimeInSeconds、getSecondsUntilResetを 適当な処理(呼ばれたら1を返す)で実装してインスタンス化する場合は、以下のようになります。

def status = [
 getRemaining: { -> 1 },
 getLimit: { -> 1 },
 getResetTimeInSeconds: { -> 1 },
 getSecondsUntilReset: { -> 1 },
] as RateLimitStatus

テスト対象クラスの一部をスタブ化

そして後はテスト対象クラスの一部をスタブ化できれば、大抵のテストは書けるようになるのかと思います。

Mockではインスタンス化したクラスのメソッドが全て実態のないものになっているので、Spyを使ってインスタンス化します。

以下は、PagableResponseListImpl#hasNext()を1度目はtrueを返し、それ以降はfalseを返すようにする実装です。

def users = Spy(PagableResponseListImpl, constructorArgs: [status, 1])
users.hasNext() >>> [true, false]

スタブの書き方は他にも色々あるので、実装時は以下などを参考にしてみてください。

また、MockとSpyの使い分けはこちらのサイトが参考になるかと思います。

@Unroll

Spockのパラメータテストの結果は、デフォルトではまとめて1テストとして出力されます。

これを各条件ごとに出力させるためには、@Unrollアノテーションをクラスかメソッドに対して付ける必要があります。
(基本は必要なメソッドに対して付けるのが良いかと思います)

@Unrollを付けていない場合、比較条件によってはエラー時にどの条件がダメだったのかがわからないので、 パラメータテストを行うメソッドには付与しておくことをお奨めします。

テストスイート

SpockにJUnitでいうところのテストスイートのような仕組みはないのですが、 Gradleでspock-coreを取り込むと、JUnitも依存ライブラリとして追加されます。

そこでJUnitでの書き方をGroovyにアレンジして、以下のようにすると、Spockでもテストスイートが実現できます。

import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.junit.runners.Suite.SuiteClasses

@RunWith(Suite)
@Suite.SuiteClasses([
    CircleChecker_CheckUserNameSpec,
    CircleChecker_CheckFollowSpec,
])

class CircleCheckerSpecSuite {
}

今回はクラス名の末尾をSpecSuiteとしましたが、いつもならAllSpecにしていたかと思います。
末尾を統一させるか、末尾を分けて目立たせるかの違いですが、好きな方でいいと思います。

テストスイートのクラスを追加した場合は、テスト数が重複して計上されないように、当該クラスをtestタスクの対象外にしておきましょう。

build.gradleに以下の記述を追加してください(include/excludeは必要なものだけでOK)。

test {
    include '**/*Test*'
    include '**/*Spec*'
    exclude '**/*TestSuite*'
    exclude '**/*SpecSuite*'
    exclude '**/*AllTest*'
    exclude '**/*AllSpec*'
}

Spockでテストを書こう!

Spockのテストコードはどうでしたか?
過去にJUnitでモック処理を書いたり、パラメータテストを書いたことがある方であれば、魅力を感じてもらえるのではないかと思います。

とはいえ、ここまで単純化できているのは、テスト対象クラスのフィールドがGroovyらしくpublicになっているから、といいますか、 protectedのメソッドでも普通に直アクセスできるからなのですが。
この振る舞いは色々議論があるでしょうが、私としてはアクセス識別子はマーカー識別子くらいの認識なので、 プログラミングする上でアクセス識別子の考え方が頭に入っていれば、この動作でも問題ないと思っています。 ただ、プログラミング初学者などにはもしかしたら良くないのかもしれないなぁとも思ったりします。 ですが、Groovyを使う人でJavaを経験してこなかった人がどれだけいるのかと考えると、この件はただの杞憂ではないかなと。

極力単純に、かつ少ないコード量でのプログラミングするためのツールとして、Spockを使ってみるのはどうでしょうか?