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))))
(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))))
(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))))
(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))
(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のテストを実行する方法について書いてみた。これによってテストクラス名とかをコピペする手間が省けるようになって便利。