$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

jsoupを使ってScalaのHTMLテンプレートのテストをする

ScalaのPlay Frameworkでの開発をしていると、HTMLテンプレートの中でちょっとした条件分岐を書くことがある。そういう時に毎回手動で確認するのも大変なので、簡単なテストくらいは書いておきたいと思った。

そこで今回はjsoupを使ってテストを書くのを試したのでメモ。

jsoupとは

https://jsoup.org/

Java用のHTMLのParserライブラリ。jQueryのselectorのような形でデータを取り出したりできるので、HTMLの確認をするのに便利に使える。Java用なのでScalaにも使える。

jsoupをテスト用に導入する

build.sbtに以下のように書いておく。バージョン番号はその時の最新を選んでおくと良い。

libraryDependencies += "org.jsoup" % "jsoup" % "1.10.3" % Test

今回テストしたいcontrollerとview

とりあえずテスト用に、簡単なcontrollerとviewを用意する。

app/controllers/HomeController.scala

package controllers

import javax.inject._
import play.api._
import play.api.mvc._

@Singleton
class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
  def index() = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.index())
  }
}

app/views/index.scala.html

@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
  <div class="div1">
    <p class="class1">hoge1</p>
    <p class="class2">fuga1</p>
  </div>
  <div class="div2">
    <p class="class1">hoge2</p>
    <p class="class2">fuga2</p>
  </div>
}

jsoupで出力されたHTMLをテストする

jsoupさえ導入されていれば、contentAsStringで取り出した文字列をparseしたら、あとはいろいろな方法でデータを取り出してテストできる。 https://github.com/shibayu36/scala-play-blog/blob/fd8354c9529c9081888fafaa42de09716cd361e9/test/controllers/HomeControllerSpec.scala

test/controllers/HomeControllerSpec.scala

package controllers

import play.api.test._
import play.api.test.Helpers._
import org.jsoup.Jsoup

class HomeControllerSpec extends test.ControllerSpec {
  describe("HomeController GET") {
    it("render the index page from a new instance of controller") {
      val controller = new HomeController(stubControllerComponents())
      val home = controller.index().apply(FakeRequest(GET, "/"))

      status(home) shouldBe OK
      contentType(home) shouldBe Some("text/html")
      contentAsString(home) should include ("Welcome to Play")

      val doc = Jsoup.parse(contentAsString(home))

      val h1 = doc.select("h1")
      h1.text shouldBe "Welcome to Play!"

      val pElems = doc.select("p")
      pElems.get(0).text shouldBe "hoge1"
      pElems.get(1).text shouldBe "fuga1"
      pElems.get(2).text shouldBe "hoge2"
      pElems.get(3).text shouldBe "fuga2"

      val pWithClass1Elems = doc.select("p.class1")
      pWithClass1Elems.get(0).text shouldBe "hoge1"
      pWithClass1Elems.get(1).text shouldBe "hoge2"

      // nested example
      val div1Elem = doc.select(".div1").first
      val pElemsInDiv1 = div1Elem.select("p")
      pElemsInDiv1.get(0).text shouldBe "hoge1"
      pElemsInDiv1.get(1).text shouldBe "fuga1"
    }
  }
}

あとjsoupをいい感じに使えば、テンプレートのテストをいろいろ行うことができる。 https://jsoup.org/cookbook/ あたりをみると、jQueryセレクタの方法で要素の取り出しは柔軟にできるし、要素からHTMLや属性を取り出したりもできるので、大体これでテストできそう。

まとめ

今回はjsoupを使ったテストについて書いてみた。これでHTMLのテンプレートのテストもできるようになったので安心できるようになった。

Play FrameworkのテストのGuiceOneAppPerSuiteとGuiceOneAppPerTestの違いを調べた

PlayFrameworkでのcontrollerのテストのやり方メモ - $shibayu36->blog; でPlay FrameworkにおけるControllerの基本的なテスト方法について学んだ。ただ、テスト用のApplicationを作るためのtraitにGuiceOneAppPerSuiteとGuiceOneAppPerTestというのがあって、どういう違いか分からなかったので調べた。

コード上のコメントが一番参考になった。

これによると、GuiceOneAppPerSuiteはSpec全体で一つのApplicationを共有していて、GuiceOneAppPerTestはすべてのテスト(FunSpecにおけるitの単位?)でApplicationを作るようになっているっぽい?

というわけで動かしてみて試した。

GuiceOneAppPerSuite

https://github.com/playframework/play-scala-seed.g8/blob/2.6.x/src/main/g8/app/controllers/HomeController.scala のcontrollerに対して、以下のコードを動かしてみる。

package controllers

import org.scalatest.{TestData, FunSpec}
import play.api.test._
import play.api.test.Helpers._
import play.api.inject.guice._
import play.api.Application
import org.scalatestplus.play.guice.{GuiceOneAppPerTest, GuiceOneAppPerSuite}

class HomeControllerSpec extends FunSpec with GuiceOneAppPerSuite with org.scalatest.Matchers {
  override def fakeApplication(): Application = {
    println("fuga")
    new GuiceApplicationBuilder().configure(Map("ehcacheplugin" -> "disabled")).build()
  }

  for (i <- 1 to 5) {
    describe("HomeController GET" + i) {
      it("render the index page from a new instance of controller" + i) {
        val controller = new HomeController(stubControllerComponents())
        val home = controller.index().apply(FakeRequest(GET, "/"))

        status(home) shouldBe OK
        contentType(home) shouldBe Some("text/html")
        contentAsString(home) should include ("Welcome to Play")
      }

      it("render the index page from a new instance of controller" + i * 10) {
        val controller = new HomeController(stubControllerComponents())
        val home = controller.index().apply(FakeRequest(GET, "/"))

        status(home) shouldBe OK
        contentType(home) shouldBe Some("text/html")
        contentAsString(home) should include ("Welcome to Play")
      }
    }
  }
}

これでテストを実行すると、fugaは一回しか出力されず、ただ一度だけApplicationが作られていそう。

$ testOnly controllers.HomeControllerSpec
[info] HomeControllerSpec:
fuga
[info] HomeController GET1
[info] - render the index page from a new instance of controller1
[info] - render the index page from a new instance of controller10
[info] HomeController GET2
[info] - render the index page from a new instance of controller2
[info] - render the index page from a new instance of controller20
[info] HomeController GET3
[info] - render the index page from a new instance of controller3
[info] - render the index page from a new instance of controller30
[info] HomeController GET4
[info] - render the index page from a new instance of controller4
[info] - render the index page from a new instance of controller40
[info] HomeController GET5
[info] - render the index page from a new instance of controller5
[info] - render the index page from a new instance of controller50
[info] ScalaTest
[info] Run completed in 4 seconds, 137 milliseconds.
[info] Total number of tests run: 10
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 10, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 10, Failed 0, Errors 0, Passed 10
[success] Total time: 5 s, completed Sep 1, 2017 8:00:19 AM

GuiceOneAppPerTest

続いて、GuiceOneAppPerTestを使ってみる。

package controllers

import org.scalatest.{TestData, FunSpec}
import play.api.test._
import play.api.test.Helpers._
import play.api.inject.guice._
import play.api.Application
import org.scalatestplus.play.guice.{GuiceOneAppPerTest, GuiceOneAppPerSuite}

class HomeControllerSpec extends FunSpec with GuiceOneAppPerTest with org.scalatest.Matchers {
  override def newAppForTest(td: TestData) = {
    println("hoge")
    new GuiceApplicationBuilder().configure(Map("ehcacheplugin" -> "disabled")).build()
  }

  for (i <- 1 to 5) {
    describe("HomeController GET" + i) {
      it("render the index page from a new instance of controller" + i) {
        val controller = new HomeController(stubControllerComponents())
        val home = controller.index().apply(FakeRequest(GET, "/"))

        status(home) shouldBe OK
        contentType(home) shouldBe Some("text/html")
        contentAsString(home) should include ("Welcome to Play")
      }

      it("render the index page from a new instance of controller" + i * 10) {
        val controller = new HomeController(stubControllerComponents())
        val home = controller.index().apply(FakeRequest(GET, "/"))

        status(home) shouldBe OK
        contentType(home) shouldBe Some("text/html")
        contentAsString(home) should include ("Welcome to Play")
      }
    }
  }
}

これも実行してみると以下のようになり、hogeがitの単位でprintされているので、itの単位でApplicationが作り直されていそう。

$ testOnly controllers.HomeControllerSpec
[info] Compiling 1 Scala source to /Users/shibayu36/development/src/github.com/shibayu36/scala-play-blog/target/scala-2.12/test-classes...
[info] HomeControllerSpec:
[info] HomeController GET1
hoge
[info] - render the index page from a new instance of controller1
hoge
[info] - render the index page from a new instance of controller10
[info] HomeController GET2
hoge
[info] - render the index page from a new instance of controller2
hoge
[info] - render the index page from a new instance of controller20
[info] HomeController GET3
hoge
[info] - render the index page from a new instance of controller3
hoge
[info] - render the index page from a new instance of controller30
[info] HomeController GET4
hoge
[info] - render the index page from a new instance of controller4
hoge
[info] - render the index page from a new instance of controller40
[info] HomeController GET5
hoge
[info] - render the index page from a new instance of controller5
hoge
[info] - render the index page from a new instance of controller50
[info] ScalaTest
[info] Run completed in 4 seconds, 925 milliseconds.
[info] Total number of tests run: 10
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 10, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 10, Failed 0, Errors 0, Passed 10
[success] Total time: 6 s, completed Sep 1, 2017 8:01:54 AM

まとめ

今回はテスト用のApplicationを作るためのtraitにGuiceOneAppPerSuiteとGuiceOneAppPerTestの違いについて、テストコードを動かしながら確認してみた。まだScala & PlayFramework初心者なのでもしかしたら間違ってるかもしれない。間違っていたら教えてください。

PlayFrameworkでのcontrollerのテストのやり方メモ

PlayFrameworkでcontrollerのテストどうやるんだろうといろいろ試してみたのでメモ。Play 2.6.3を使っている。

基本的なやり方

play-scala-seed.g8というのに、基本的なControllerとそのテスト方法について書かれているので、それを真似れば良い。

Play 2.6ではControllerは@InjectでControllerComponentsを渡す形式が一般的なので、stubControllerComponents()で得られるテスト用のControllerComponentsを渡せばテストできるっぽい。あとはFakeRequestでテストしたいRequestを作り、いろいろとテストしていくだけ。

自分でControllerのテストの基底クラスを作る

とりあえず真似れば良いのだけど、それだとあまり理解できないので、自分好みのテストのやり方になるようにControllerのテストの基底クラスを作ってみた。PlaySpecの実装を見ながら作った。

test/test/ControllerSpec.scala

package test

import org.scalatest.FunSpec
import org.scalatestplus.play.guice.GuiceOneAppPerTest

abstract class ControllerSpec extends FunSpec with GuiceOneAppPerTest with org.scalatest.Matchers

PlaySpecとの違いは

  • 普通にdescribeとかitとかを使ってテストをしたかったので、WordSpecではなくてFunSpecをmixinした
  • mustBeじゃなくてshouldBeを使いたかったのでMustMatchersではなくてMatchersをmixinした
  • あとはアプリケーションを起動できるように(?)、GuiceOneAppPerTestをmixinした
    • GuiceOneAppPerTestの役割はまだちゃんと理解は出来ていない

これでこの基底クラスを使って以下のようにテストできる。

test/controllers/HomeControllerSpec.scala

package controllers

import play.api.test._
import play.api.test.Helpers._

class HomeControllerSpec extends test.ControllerSpec {
  describe("HomeController GET") {
    it("render the index page from a new instance of controller") {
      val controller = new HomeController(stubControllerComponents())
      val home = controller.index().apply(FakeRequest(GET, "/"))

      status(home) shouldBe OK
      contentType(home) shouldBe Some("text/html")
      contentAsString(home) should include ("Welcome to Play")
    }
  }
}