$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

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")
    }
  }
}

ScalaTestのshouldBeとmustBeの違いは何か

ScalaTestのMatchers系にshouldBeとmustBeがあって、どういう違いがあるのだろうと気になったので調べてみたのでメモ。

http://www.scalatest.org/user_guide/using_matchers を読むと、

Trait MustMatchers is an alternative to Matchers that provides the exact same meaning, syntax, and behavior as Matchers, but uses the verb must instead of should. The two traits differ only in the English semantics of the verb: should is informal, making the code feel like conversation between the writer and the reader; must is more formal, making the code feel more like a written specification.

と書いていて、両方共機能的違いはまったくなく、単にshouldとmustというメソッド名が違うというだけだった。まじか...

【Scala】Emacsで現在編集している部分のテストを実行する

Scalaのテストを実行する時、sbtを使うと、特定のテストクラスだけの実行や、特定のテストケースだけの実行ができる。やり方はScalatest: 特定のテストケースだけ実行したい - Qiita で紹介されているとおり。

ただ、コードはテキストエディタで書いているので、このコマンドを使うとしても

  • 編集を終える
  • 今のファイルのテストクラス名とdescribeの文字列をコピーする
  • ターミナルを開いて、sbtでtestOnlyを使ってコピーした文字列を貼り付けて実行

のように、結構面倒という問題がある。

そこでEmacsで現在編集している部分のテストを実行するユーティリティを作り、編集 -> テスト -> 編集のループを回しやすくしてみた。今回はそのことについてご紹介。

今回のユーティリティで出来ること

以下のように、現在ファイルのテストクラスだけEmacs上で実行したり

さらに、現在のカーソルが存在するdescribeのテストだけ実行したりできる。以下のアニメーションではScalaTestExample#listだけ実行されている。

実現方法

現在編集中のバッファから、パッケージ名・テストクラス名・describeを抜き出し、それをsbt-modeに渡すことで実現できる。

sbt-mode というものを使うと、Emacs上でsbtを動かし、それに対してコマンドを送ることができる。まずこれをインストールする。

M-x package-install RET sbt-mode

後は以下のような実装を行うと、sbt/test-only-current-specで現在のバッファのテストクラスを実行でき、sbt/test-only-current-describeで現在カーソルがいるdescribeのテストだけを実行できる。

;;; 現在のバッファのテストクラスを実行する
(defun sbt/test-only-current-spec ()
  "Run test with current file."
  (interactive)
  (sbt-command
   (format "testOnly %s" (scala/find-spec-name-with-package-current-buffer))))

;;; 現在カーソルがいるdescribeのテストだけを実行する
(defun sbt/test-only-current-describe ()
  "Run current describe test"
  (interactive)
  (sbt-command
   (format "testOnly %s -- -z \"%s\""
           (scala/find-spec-name-with-package-current-buffer)
           (scala/find-nearest-spec-describe-current-buffer))))

;;; 現在のバッファからpackage名とテストクラス名を結合したものを返す
(defun scala/find-spec-name-with-package-current-buffer ()
  "Find spec name with package in current buffer."
  (interactive)
  (let* ((package-name (scala/find-package-name-current-buffer))
         (spec-name (scala/find-spec-name-current-buffer)))
    (if (string= package-name "")
        spec-name
      (format "%s.%s" package-name spec-name))))

;;; 現在のバッファからpackage名を抜き出す
(defun scala/find-package-name-current-buffer ()
  "Find package name in current buffer"
  (interactive)
  (let* ((matched-package ""))
    (save-excursion
      (when (re-search-backward "^package \\(.+\\)$" nil t)
        (setq matched-package (match-string 1))))
    matched-package))

;;; 現在のバッファからテストクラス名を抜き出す
(defun scala/find-spec-name-current-buffer ()
  "Find spec name of current buffer."
  (interactive)
  (let* ((matched-spec-name ""))
    (save-excursion
      (when (re-search-backward "^class \\([^ ]+Spec\\) " nil t)
        (setq matched-spec-name (match-string 1))))
    matched-spec-name))

;;; 現在カーソルがいるdescribe名を抜き出す
(defun scala/find-nearest-spec-describe-current-buffer ()
  (interactive)
  (let* ((matched-describe-name ""))
    (save-excursion
      (when (re-search-backward "\\bdescribe(\"\\([^\"]+\\\)\")" nil t)
        (setq matched-describe-name (match-string 1))))
    matched-describe-name))

後は好きなキーバインドを割り当てる。

(define-key scala-mode-map (kbd "C-c C-t") 'sbt/test-only-current-spec)
(define-key scala-mode-map (kbd "C-c t") 'sbt/test-only-current-describe)

また、僕は popwin で表示するのが好きなので、以下のようにsbt-modeのバッファはポップアップするように設定した。

(push '("\*sbt\*" :regexp t :height 0.5 :stick t) popwin:special-display-config)

技術的Tips

今回の実装は、「現在バッファの特定の文字列を抜き出す」ということが出来れば簡単に実装できる。やり方はこんな感じ。

(save-excursion
  (when (re-search-backward "^class \\([^ ]+Spec\\) " nil t)
    (setq matched-spec-name (match-string 1))))

まず、save-excursionでバッファの状態を保存しておく。これがないと文字列を探索した時にカーソルの位置が変わってしまう。

次にre-search-backwardを使って、現在カーソルより上の文字列を正規表現で探索する。抜き出したい部分を括弧を使って取り出せるようにしておく。

後はmatch-stringを使って、括弧で囲っていた部分を取り出す。1番目の括弧の中身を取り出したいので、1を指定している。

まとめ

今回はEmacsで現在編集している部分のScalaのテストを実行する方法について書いてみた。これによってテストクラス名とかをコピペする手間が省けるようになって便利。