Run a single ScalaTest case with Emacs and sbt-mode
Too long; didn't read
Run a ScalaTest test suite or the test a point with sbt-mode by loading the Elisp in this file.
The problem
I was writing up about my Scala setup for Emacs and I remembered one feature of IntelliJ that I am somewhat missing: you can run a single suite of tests or a test at point with one click.
While I am working I need to check if tests still pass after I change
something, so I mostly use sbt's testQuick
to run only the tests
affected by my work. Sometimes though testQuick
runs a lot of unit
tests that I do not really care about at the moment, and slows my
feedback loop.
It is a problem indeed
A few people are interested in running this feature from the sbt command line: https://stackoverflow.com/questions/11159953/scalatest-in-sbt-is-there-a-way-to-run-a-single-test-without-tags Hopefully some of these are Emacs users! Aaand, who said Emacs cannot have this as well?!?
And there is a solution
So I decided to apply some Elisp magic. (By the way, I am astonished: I wrote the following code on my ereader before sleeping and when I coded it down worked immediately!)
The plan of battle: we need to feed the special command mentioned in
the StackOverflow answer to our sbt=command
function provided by
sbt-mode
. We will start easy by running a whole test suite and then
we will refine by running a single test case, because they are both
useful features.
Let's start from the little function that returns the command string:
(ert-deftest sbt/get-testonly-file_filename_testOnlyFile () (should (string= (sbt/get-testonly-file "testSpec") "testOnly *testSpec"))) (defun sbt/get-testonly-file (&optional file) "Return FILE formatted in a sbt testOnly command." (--> (or file (file-name-base)) (format "testOnly *%s" it)))
Since we are speaking of tests, let's show a bit of test driven development with ERT.
Given that, running a test suite is just applying sbt-command
to the
string:
(defun sbt/run-test-file (&optional file) (interactive) (sbt-command (sbt/get-testonly-file file)))
Now that we are warmed up, let's deal with a single test case. First we need the test case name:
(defun sbt/get-testcase-name () "Get Scala test case nearby point." (save-excursion (let* ((line (thing-at-point 'line t)) (on-testcase-p (and (s-contains? "\"" line) (s-contains? "{\n" line))) (get-testcase-name (lambda (l) (--> l (s-split "\"" it) reverse second)))) (if on-testcase-p (funcall get-testcase-name line) (progn (search-backward "{\n") (funcall get-testcase-name (thing-at-point 'line t)))))))
The above on-testcase-p
represents my effort of finding a pattern in
the (too many) styles to run a ScalaTest suite: if it contains a quote
and an opening curly bracket with a newspace, I say this line is
stating a test name.
And get-testcase-name
is a concise way to get the name. Indeed, we
want the contents of the last string before the curly bracket. For
example in:
"The Hello object" should "say hello" in { Hello.greeting shouldEqual "hello" }
We need "say hello", so the last string from the opening curly
bracket. Also, knowing myself, I want to get the test name even if I
am in the body of the test. For this reason we got a if
in the code.
Finally again just some plumbing with sbt=command
:
(defun sbt/run-testcase-at-point () "Run Scala test case at point." (interactive) (--> (sbt/get-testonly-file) (format "%s -- -z \"%s\"" it (sbt/get-testcase-name)) sbt-command))
Done! I recommend to bind these functions to some comfortable keybindings like this:
(use-package sbt-mode :commands sbt-start sbt-command :bind (:map scala-mode-map ("C-c s c" . sbt-command) ("C-c s t" . sbt/run-testcase-at-point) ("C-c s T" . sbt/run-test-file)))
Conclusion
You see? Emacs can do it! And you can do it too: just load those snippets in your configuration and run only the Scala tests you need. Feel free to get in touch if you are a Scala developer using Emacs and have good tricks to share!
Happy hacking!
Update 2020-10-25
Thanks to @VlachJosef I discovered that sbt-mode
already has these
features in a little extras library:
https://github.com/hvesalai/emacs-sbt-mode/blob/master/sbt-mode-hydra.el#L464-L490.
Note that library requires hydra
: I do not use it so I will keep my hack :)