Testing Emacs code that modifies buffers

October 2017

2022 Update: I have since made an improved version of the code in this article. This article is still useful to read as motivation for the problem.


I was writing some functions to change variable names from camelCase to snake_case, and back again. (For some reason, Java code uses camelCase, and SQL colmns are named with snake_case) Because this was nontrivial code, I wanted to test it.

If the functions took a string as an argument, and returned the modified value, I'd be able to write some simple tests. These tests would call the function on some strings, and check the return value. But instead, the functions we want to test modify the buffer instead of returning a value. So what can we do to test this code? We could rewrite the functions to be small wrappers around functions that do the "actual work" of changing cases. It would be unfortunate to have to rewrite the code to be able to test it, but at least we'd be able to write some tests.

But even wrapping the functions this way wouldn't be enough! The functions change point, which is not testable by checking the return value from such a wrapped function!

What would I do if I was testing it by hand? I'd insert some text in a buffer, call my function on it to modify the buffer. I'd then examine the text in the buffer. Here's the outline of what I want to do:

In a temp buffer:
  insert text
  move point to the beginning of the buffer
  call the function being tested
  return the entire buffer as a string

That maps to emacs lisp code straightforwardly:

(with-temp-buffer
  (insert "hi_mom")
  (goto-char (point-min))
  (camelcase-dwim 1)
  (buffer-string))

Great! This code now calls our function under test on a specific input string, and returns the value of the buffer at the end. Let's set it up as an ERT test:

(ert-deftest camelcase-dwim/single-word-from-snake-case ()
  (should (equal "hiMom"
                 (with-temp-buffer
                   (insert "hi_mom")
                   (goto-char (point-min))
                   (camelcase-dwim 1)
                   (buffer-string)))))

But look how repetitive a second test would be!

(ert-deftest camelcase-dwim/two-words-from-snake-case ()
  (should (equal "hiMom howAreYou"
                 (with-temp-buffer
                   (insert "hi_mom how_are_you")
                   (goto-char (point-min))
                   (camelcase-dwim 2)
                   (buffer-string)))))

That's a lot of copied code. Let's write a macro that does this for us:

(defmacro on-temp-buffer (string &rest body)
  "Insert STRING into a temp buffer, then run BODY on the temp buffer.

Point starts at the beginning of the buffer, and after running BODY,
the entire buffer is returned as a string."
  (declare (indent 0) (debug t))
  `(with-temp-buffer
     (insert ,string)
     (goto-char (point-min))
     ,@body
     (buffer-string)))

Now, the test code is much shorter:

(ert-deftest camelcase-dwim/single-word-from-snake-case ()
  (should (equal "hiMom"
                 (on-temp-buffer
                   "hi_mom"
                   (camelcase-dwim 1)))))

Awesome, this test is now way easier to read because it only has the logic we care about. But we're not done. Remember that #'camelcase-dwim moves point? I want to make sure point is moved to the right place. So I wrote a similar macro which returns (point) instead of (buffer-string):

(defmacro on-temp-buffer-point (string &rest body)
  "Insert STRING into a temp buffer, then run BODY on the temp buffer.

Point starts at the beginning of the buffer, and after running BODY,
\(point) is returned."
  (declare (indent 0) (debug t))
  `(with-temp-buffer
     (insert ,string)
     (goto-char (point-min))
     ,@body
     (point)))

It can be used as follows:

(ert-deftest camelcase-dwim/basic-snake-case-with-other-words-check-point ()
  (should (equal 6
                 (on-temp-buffer-point
                   "hi_mom and_other stuff"
                   (camelcase-dwim 1)))))

So now we have two macros that can be used to test code that modifies an Emacs buffer, without having to change the functions being tested. And that's our goal.

If you want to hear when I publish more, sign up for my mailing list!

    previous

    < Presenting zpresent, a presentation framework for Emacs
    tag: emacs

    next

    Reduce your Emacs config (by contributing it upstream) >