sbt プラグインをテストする
テストの話をしよう。一度プラグインを書いてしまうと、どうしても長期的なものになってしまう。新しい機能を加え続ける(もしくはバグを直し続ける)ためにはテストを書くのが合理的だ。だけど、ビルドツールのプラグインのテストなんてどうやって書けばいいんだろう?もちろん飛ぶんだよ。
scripted test framework
sbt は、scripted test framework というものが付いてきて、ビルドの筋書きをスクリプトに書くことができる。これは、もともと 変更の自動検知や、部分コンパイルなどの複雑な状況下で sbt 自体をテストするために書かれたものだ:
ここで、仮に B.scala を削除するが、A.scala には変更を加えないものとする。ここで、再コンパイルすると、A から参照される B が存在しないために、エラーが得られるはずだ。 [中略 (非常に複雑なことが書いてある)]
scripted test framework は、sbt が以上に書かれたようなケースを的確に処理しているかを確認するために使われている。
正確には、このフレームワークは siasia として知られる Artyom Olshevskiy 氏により移植された scripted-plugin 経由で利用可能だが、これは正式なコードベースに取り込まれている。
ステップ 1: snapshot
scripted-plugin はプラグインをローカルに publish するため、まずは version を -SNAPSHOT なものに設定しよう。
ステップ 2: scripted-plugin
次に、scripted-plugin をプラグインのビルドに加える。project/scripted.sbt
:
libraryDependencies <+= (sbtVersion) { sv =>
"org.scala-sbt" % "scripted-plugin" % sv
}
以下を scripted.sbt
に加える:
ScriptedPlugin.scriptedSettings
scriptedLaunchOpts := { scriptedLaunchOpts.value ++
Seq("-Xmx1024M", "-XX:MaxPermSize=256M", "-Dplugin.version=" + version.value)
}
scriptedBufferLog := false
ステップ 3: src/sbt-test
src/sbt-test/<テストグループ>/<テスト名>
というディレクトリ構造を作る。とりあえず、src/sbt-test/<プラグイン名>/simple
から始めるとする。
ここがポイントなんだけど、simple
下にビルドを作成する。プラグインを使った普通のビルド。手動でテストするために、いくつか既にあると思うけど。以下に、build.sbt
の例を示す:
import AssemblyKeys._
version := "0.1"
scalaVersion := "2.10.2"
assemblySettings
jarName in assembly := "foo.jar"
これが、project/plugins.sbt
:
{
val pluginVersion = System.getProperty("plugin.version")
if(pluginVersion == null)
throw new RuntimeException("""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin)
else addSbtPlugin("com.eed3si9n" % "sbt-assembly" % pluginVersion)
}
これは JamesEarlDouglas/xsbt-web-plugin@feabb2 から拝借してきた技で、これで scripted テストに version を渡すことができる。
他に、src/main/scala/hello.scala
も用意した:
object Main extends App {
println("hello")
}
ステップ 4: スクリプトを書く
次に、好きな筋書きを記述したスクリプトを、テストビルドのルート下に置いた test
というファイルに書く。
# ファイルが作成されたかを確認
> assembly
$ exists target/scala-2.10/foo.jar
スクリプトの文法は ChangeDetectionAndTesting に記述されている通りだけど、以下に解説しよう:
#
は一行コメントを開始する>
name
はタスクを sbt に送信する(そして結果が成功したかをテストする)$
name arg*
はファイルコマンドを実行する(そして結果が成功したかをテストする)->
name
タスクを sbt に送信するが、失敗することを期待する-$
name arg*
ファイルコマンドを実行するが、失敗することを期待する
ファイルコマンドは以下のとおり:
touch
path+
は、ファイルを作成するかタイムスタンプを更新するdelete
path+
は、ファイルを削除するexists
path+
は、ファイルが存在するか確認するmkdir
path+
は、ディレクトリを作成するabsent
path+
は、はファイルが存在しないことを確認するnewer
source target
は、source
の方が新しいことを確認するpause
は、enter が押されるまで待つsleep
time
は、スリープするexec
command args*
は、別のプロセスでコマンドを実行するcopy-file
fromPath toPath
は、ファイルをコピーするcopy
fromPath+ toDir
は、パスを相対構造を保ったままtoDir
下にコピーするcopy-flat
fromPath+ toDir
は、パスをフラットにtoDir
下にコピーする
ということで、僕のスクリプトは、assembly
タスクを実行して、foo.jar
が作成されたかをチェックする。もっと複雑なテストは後ほど。
ステップ 5: スクリプトを実行する
スクリプトを実行するためには、プラグインのプロジェクトに戻って、以下を実行する:
> scripted
これはテストビルドをテンポラリディレクトリにコピーして、test
スクリプトを実行する。もし全て順調にいけば、まず publish-local
の様子が表示され、以下のようなものが表示される:
Running sbt-assembly / simple
[success] Total time: 18 s, completed Sep 17, 2011 3:00:58 AM
ステップ 6: カスタムアサーション
ファイルコマンドは便利だけど、実際のコンテンツをテストしないため、それだけでは不十分だ。コンテンツをテストする簡単な方法は、テストビルドにカスタムのタスクを実装してしまうことだ。
上記の hello プロジェクトを例に取ると、生成された jar が “hello” と表示するかを確認したいとする。sbt.Process
を用いて jar を走らせることができる。失敗を表すには、単にエラーを投げればいい。以下に build.sbt
を示す:
import AssemblyKeys._
version := "0.1"
scalaVersion := "2.10.2"
assemblySettings
jarName in assembly := "foo.jar"
TaskKey[Unit]("check") <<= (crossTarget) map { (crossTarget) =>
val process = sbt.Process("java", Seq("-jar", (crossTarget / "foo.jar").toString))
val out = (process!!)
if (out.trim != "bye") error("unexpected output: " + out)
()
}
ここでは、テストが失敗するのを確認するため、わざと “bye” とマッチするかテストしている。 空行を入れると、ブロックの終わりだと解釈されるので気をつけよう。
これが test
:
# ファイルが作成されたかを確認
> assembly
$ exists target/foo.jar
# hello って言うか確認
> check
scripted
を走らせると、意図通りテストは失敗する:
[info] [error] {file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_cdd1b3c4/simple/}default-0314bd/*:check: unexpected output: hello
[info] [error] Total time: 0 s, completed Sep 21, 2011 8:43:03 PM
[error] x sbt-assembly / simple
[error] {line 6} Command failed: check failed
[error] {file:/Users/foo/work/sbt-assembly/}default-373f46/*:scripted: sbt-assembly / simple failed
[error] Total time: 14 s, completed Sep 21, 2011 8:00:00 PM
テストビルド間でアサーションを再利用したい場合は、full configuration を用いて、カスタムのビルドクラスを継承することができる。
ステップ 7: テストをテストする
慣れるまでは、テスト自体がちゃんと振る舞うのに少し時間がかかるかもしれない。ここで使える便利なテクニックがいくつある。
まず最初に試すべきなのは、ログバッファリングを切ることだ。
> set scriptedBufferLog := false
これにより、例えばテンポラリディレクトリの場所などが分かるようになる:
[info] [info] Set current project to default-c6500b (in build file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_8d950687/simple/project/plugins/)
...
テスト中にテンポラリディレクトリを見たいような状況があるかもしれない。test
スクリプトに以下の一行を加えると、scripted はエンターキーを押すまで一時停止する:
$ pause
もしうまくいかなくて、 sbt/sbt-test/sbt-foo/simple
から直接 sbt
を実行しようと思っているなら、それは止めたほうがいい。Mark がコメント欄で教えてくれた通り、正しいやり方はディレクトリごと別の場所にコピーしてから走らせることだ。
ステップ 8: インスパイアされる
sbt プロジェクト下には文字通り 100+ の scripted テストがある。色々眺めてみて、インスパイアされよう。
例えば、以下に by-name と呼ばれるものを示す:
> compile
# change => Int to Function0
$ copy-file changes/A.scala A.scala
# Both A.scala and B.scala need to be recompiled because the type has changed
-> compile
xsbt-web-plugin や sbt-assemlby にも scripted テストがある。
これでおしまい!プラグインをテストしてみた経験などを聞かせて下さい!