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を指定している。