Where parallels cross

Interesting bits of life

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 :)

Comments