Buttercup is a testing framework for emacs-lisp. It is used by large projects like Clojure's CIDER to write clean, concise, and descriptive tests.
I introduce Buttercup and build up to advanced usages with the faint, unlikely dream that some Emacs programmer decides to add tests to their library…
What is Buttercup?
Introduction
Buttercup's entry points are: describe
, it
, and expect
.
We describe
a test suite with a name. Test cases within the possibly nested
suites are done with it
and assertions as expect
blocks within.
(describe "Four"
(describe "comparisons"
(it "is greater than one"
(expect (> 4 1)))
(it "and less than five"
(expect 4 :to-be-less-than 5)))
(it "is a number"
(expect (numberp 4))))
passes with testing output:
Four comparisons is greater than one (0.24ms) and less than five (0.12ms) is a number (0.09ms)
Setup and Teardown
Buttercup provides before-each
, after-each
, before-all
, and after-all
to
reduce boilerplate with setting up and tearing down test suites.
(describe "Lisp mode syntax"
(before-all (set-syntax-table lisp-mode-syntax-table))
(after-each (delete-region (point-min) (point-max)))
(it "sets comments"
(insert ";; foo")
(expect (nth 4 (syntax-ppss))))
(it "sets strings"
(insert "\"foo\"")
(backward-char)
(expect (nth 3 (syntax-ppss)))))
Matchers
The expect
has more utility than simple tests of truth. Matchers are
keywords that tailor the expectation.
Some example matcher expansions:
- :to-be
-
(eq foo bar)
- :to-equal
-
(equal foo bar)
- :to-be-in
-
(member foo bar)
- :to-be-close-to
-
(foo bar precisision)
- :to-throw
-
(expr &optional signal signal-args)
Some other more advanced matchers include: :to-have-same-items-as
, :to-match
, and :to-have-been-called
.
These matchers may be combined too: eg. (expect 4 :not :to-be-greater-than 5)
.
Matchers are more than just transforms+comparisons. They give information about the failure.
(describe "Example Matchers"
(it "regexes"
(expect (s-concat "foo" "bar")
:to-match (rx word-start "foo" word-end))))
Expected `(s-concat "foo" "bar")' with value "foobar" to match the regexp "\\<foo\\>", but instead it was "foobar".
Running It
I recommend using Cask
and executing tests with cask exec buttercup -L .
in the project root.
For example, have a file named Cask
in the project root with:
(source gnu)
(source melpa)
(package-file "test-stuff-i-beg-you-mode.el")
;; Project Dependencies
(depends-on "dash")
;; Additional Testing Dependencies
(development
(depends-on "buttercup")
(depends-on "faceup"))
A folder named test/
should be present and contain test-stuff-i-beg-you-mode-test.el
.
This file should have your tests, set up the load path if needed, and require everything you need.
Lastly I will mention some other useful features before diving in to Buttercup:
-
Variables can be defined with let syntax with
:var
indescribe
blocks. -
Buttercup has good support for spying on function calls.
-
Adding an
x
, so it'sxit
andxdescribe
, mark the test as pending so it won't be executed.
Case Study: Testing Indentation
You have written yet-another-lisp-like-mode
you affectionately call yall-mode
and want to test its indentation.
Lets write a skeleton to test the simplest cases:
;; Want to test these two cases:
;; (foo
;; bar)
;; (foo bar
;; baz)
(describe "Indentation"
(before-all (setq indent-line-function #'yall-indent-line))
(describe "standard cases"
(it "opening line has one sexp - so indentation doesn't carry"
(expect ???))
(it "opening line has two+ sexps - so indentation carries"
(expect ???))))
To test indentation - all we need is the text we expect, as the text alone determines the indent.
Buttercup allows us to achieve this via custom matchers. We can bypass all boilerplate and write our expectations as simply as:
(expect "
(foo
bar)
" :indented)
The macro buttercup-define-matcher
allows defining our own matcher, that will
perform transforms, assertions, and give descriptive failures.
Lets implement our :indented
matcher:
(defun yall-trim-indent (text)
"Remove indentation from TEXT."
(->> text s-lines (-map #'s-trim-left) (s-join "\n")))
(defun yall-buffer-string ()
"Return buffer as text with beginning and ending empty space trimmed."
(s-trim (buffer-substring-no-properties (point-min) (point-max))))
(buttercup-define-matcher :indented (text)
(let* ((text (s-trim (funcall text)))
(text-no-indent (yall-trim-indent text)))
(insert text-no-indent)
(indent-region-line-by-line (point-min) (point-max))
(let ((text-with-indent (yall-buffer-string)))
(delete-region (point-min) (point-max))
(if (string= text text-with-indent)
t
`(nil . ,(format "\nGiven indented text \n%s\nwas instead indented to \n%s\n"
text text-with-indent))))))
Now we can see the power of buttercup when we accidentally write:
(describe "Indentation"
(before-all (setq indent-line-function #'yall-indent-line))
(describe "standard cases"
(it "opening line has two+ sexps - so indentation carries"
(expect "
(foo bar
baz)
" :indented))))
and are given the failure:
FAILED: Given indented text (foo bar baz) was instead indented to (foo bar baz)
We know exactly what went wrong, with nearly all the implementation details
separated from the testcase with boilerplate just :indented
.
Testing Emacs programs doesn't have to be painful - buttercup is a great and battle-tested library for writing quality Emacs programs.