play test
で JavaScript のテストも一緒に走らせる環境を作ろうと悪戦苦闘した結果、
だいぶ無理やりなものになってしまった。
正攻法というか、イケてるやり方は sbt-jasmine-plugin なり何なり
JavaScript テスティングフレームワークの sbt プラグインを入れて連携させる、とかになりそうだけど。
自分のやり方が悪いとは思うけど、sbt-jasmine-plugin を入れた環境構築がなかなか上手く行かず、
別のやり方を探そうとしたメモを以下に。
仮に以下の CoffeeScript をテストしたいとする。
$ -> console.log "Hello world!"
/app/assets/javascripts/ に配置する。
(/app/assets/ 以下に配置すると、Play がビルド時に js にコンパイルしてくれる。
上記の CoffeeScript コードは /assets/javascripts/hello.min.js または /assets/javascripts/hello.js として使える。)
使用する HTML テンプレートはこちら。
<!DOCTYPE html>
<html>
<head>
<title>Hello world!</title>
<script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script>
<script src="@routes.Assets.at("javascripts/hello.min.js")" type="text/javascript"></script>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
ブラウザアクセスし、コンソールに「Hello world!」が出力されることを確認。
Play の specs2 では Selenium WebDriver が使えるので、
ブラウザアクセスして JavaScript を実行した結果を取得しようとしてみる。
とはいえ、コンソールの内容をテストコード中でどうやって取得したらいいのか分からなかったので、
とりあえずブラウザアクセスするだけのテストを流してみる。
package views
import org.specs2.mutable.Specification
import play.api.test.TestServer
import play.api.test.Helpers.{ running, HTMLUNIT }
class CoffeeScriptSpec extends Specification {
private val testPort = 3333
"hello" should {
"run in a browser" in {
running(TestServer(testPort), HTMLUNIT) { browser =>
browser.goTo(s"http://localhost:${testPort}/hello")
success
}
}
}
}
実行してみると…あ、あれ?失敗だ。
スタックトレースを見てみると、
[error] ReferenceError: "console" is not defined. (http://localhost:3333/assets/javascripts/hello.min.js#1)
うーむ、 console が定義されていないと言われているようだ。
これは Play が使用している Selenium のバージョンの関係かな?
仕方がないのでテスト時のみ console.log 関数を作成するようにしてみた。
コンソールの内容を HTML に書き出して、それをテストコード中で取得してアサートすればいいのでは、
と思い立ってこんな感じに。
まず、console.log を上書き定義するようなコードを書く。
window.console = log: (msg) ->
$("body").append "<div id=\"__console\">" if $("#__console").size() is 0
$("#__console").append "#{msg}<br />"
これはテスト時にしか呼ばれたくないので、HTMLテンプレート側でこうする。
@(title: String, scriptFiles: Seq[String] = Seq())(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script>
@if(play.api.Play.isTest(play.api.Play.current)) {
<script src="@routes.Assets.at("javascripts/for-unittest.js")" type="text/javascript"></script>
}
@for(scriptFile <- scriptFiles) {
<script src="@routes.Assets.at(s"javascripts/${scriptFile}")" type="text/javascript"></script>
}
</head>
<body>
@content
</body>
</html>
@main("Hello world!", Seq("hello.min.js")) {
<p>Hello world!</p>
}
hello.scala.html 以外でも同じ仕掛けにしたいので main.scala.html で共通化。
テストモードの時のみ for-unittest.js が読み込まれるようにする。
普通にブラウザアクセスすると、変更前と変わりなくコンソールに「Hello world!」が出力される。
が、テスト時には <div id="__console"></div> という要素を作って
その中にコンソール出力する内容を書いていくので、その内容をテストコードで拾う。
package views
import org.specs2.mutable.Specification
import play.api.test.TestServer
import play.api.test.Helpers.{ running, HTMLUNIT }
class CoffeeScriptSpec extends Specification {
private val testPort = 3333
"hello" should {
"run in a browser" in {
running(TestServer(testPort), HTMLUNIT) { browser =>
browser.goTo(s"http://localhost:${testPort}/hello")
browser.$("#__console").getText must_== "Hello world!"
}
}
}
}
テスト成功!
…これでいいのか、という気はしないでもないが、とりあえずやりたいことはできた…か。
ちなみに、テストコードの中で任意の JavaScript を実行することもできるので、
以下のようなクラスをテストする場合、
class @LogWriter
constructor: (@prefix, @suffix) ->
write: (msg) -> console.log @prefix + msg + @suffix
このようにテストを書ける。
package views
import org.specs2.mutable.Specification
import play.api.test.TestServer
import play.api.test.Helpers.{ running, HTMLUNIT }
class CoffeeScriptSpec extends Specification {
private val testPort = 3333
"logwriter" should {
"run in a browser" in {
running(TestServer(testPort), HTMLUNIT) { browser =>
browser.goTo(s"http://localhost:${testPort}/logwriter")
browser.executeScript("""
var writer = new LogWriter("[", "]");
writer.write("hello");
writer.write("world");
""")
println(browser.$("#__console").getText)
browser.$("#__console").getText must_== """[hello]
[world]"""
}
}
}
}
logwriter.coffee で作成したクラスをexecuteScript で生成・実行している。
ちょっとハマった点としては、生成される div 要素にスタイルシートで
display:none をつけていたけど、
その場合 browser.$("#__console").getText の結果が空文字になってしまっていた。
getText は表示されるかどうかに関わらず中身のテキストが取れるものと思っていたのだが…。
どうせテストモードでしか使用されないため、 display:none は指定しないことで回避。