Emacs Lispで書いたスクリプトをert.elでユニットテストしてみました。

Emacs Lispでも、少し込み入ったスクリプトを書いていると、 ユニットテストをしたくなることがあります。 少し調べたところert.elというライブラリが見つかり、使いやすく便利 だったのでご紹介したいと思います。

(なお、この記事の内容はDebian 5 (lenny)上のEmacs 23に基づいています。)

ert.el

ert.el(Emacs Lisp Regression Testing)は、いくつかあるEmacs Lisp向け ユニットテスティングツールのひとつです。Emacs trunkに入ったそうなので、 そのうち標準で使えるようになるはずです。さしあたって単体で手に入れる こともできます。

対話的にはもちろん、非対話的にも実行できるのが便利なところです。

ert.elの簡単な使い方

いくつかの使い方がありますが,次のような構造を持ったテストスクリプトを 書いて、Emacsのバッチモードで実行する方法が分かりやすいでしょう。

;; test-foo.el

;; 先頭でertとテスト対象をロードしておく
(require 'ert)
(require 'TEST-TARGET)

;; ert-deftestでテストを定義する
(ert-deftest TESTNAME ()
            (should (equal EXPECTED
                           ACTUAL)))

;; 末尾にバッチ処理の指示を書いておく
(ert-run-tests-batch-and-exit)

単純な例を書いてみます。

テスト対象のスクリプト(my-fold.el)は次のようなものだとします。

;; my-fold.el
(defun my-foldl (f seed ls)
  (mapc (lambda (x) (setq seed (funcall f x seed))) ls)
        seed)
(defun my-foldr (f seed ls)
  (my-foldl f seed (reverse ls)))
(provide 'my-fold)

テストスクリプト(test-my-fold.el)は次のような内容にします。

;; test-my-fold.el
(require 'ert)
(require 'my-fold)
(ert-deftest test-my-foldl ()
             (should (equal '(3 2 1)
                            (my-foldl 'cons '() '(1 2 3)))))
(ert-deftest test-my-foldr ()
             (should (equal '(1 2 3)
                            (my-foldr 'cons '() '(1 2 3)))))
(ert-run-tests-batch-and-exit)

これらの2つのファイルをカレントディレクトリに置いて、 コマンドラインから次のようにemacsを実行します。

$ emacs --directory . --batch --quick --eval '(load-file "test-my-fold.el")'

次のように表示されればテスト成功です。

Loading /path/to/test-my-fold.el (source)...
Running 2 tests (2011-04-11 xx:xx:xx)
   passed  1/2  test-my-foldl
   passed  2/2  test-my-foldr

Ran 2 tests, 2 results as expected (2011-04-11 xx:xx:xx)

すべてのテストについてpassedと表示されていますね。

一方、テストが失敗したときは、失敗の内容を教えてくれるメッセージが 表示されます。 試しに、my-fold.elの中のmy-foldr関数の2行目を、次のように変更して わざと間違った値を返すようにしてみます。

 (defun my-foldr (f seed ls)
-  (my-foldl f seed (reverse ls)))
+  (my-foldl f seed ls))

この状態でテストを実行すると、次のようなメッセージが表示されます。

Loading /path/to/test-my-fold.el (source)...
Running 2 tests (2011-04-11 xx:xx:xx)
   passed  1/2  test-my-foldl
Test test-my-foldr backtrace:
...(省略)...
Test test-my-foldr condition:
(ert-test-failed
 ((should
   (equal '...
    (my-foldr ... ... ...)))
  :form
  (equal
   (1 2 3)
   (3 2 1))
  :value nil :explanation
  (list-elt 0
            (different-atoms
             (1 "#x1" "?")
             (3 "#x3" "?")))))
  FAILED  2/2  test-my-foldr

Ran 2 tests, 1 results as expected, 1 unexpected (2011-04-11 xx:xx:xx)

1 unexpected results:
   FAILED  test-my-foldr

メッセージの末尾にFAILED test-my-foldrとあることから,テストのうち test-my-foldrが失敗したことが分かります。

さかのぼってtest-my-foldr:form:explanationのあたりを見ると、 (1 2 3)を期待したのに(3 2 1)が返ってきているらしいことが分かります。

こういった情報を手がかりに、テスト対象やテストスクリプトを直したり発展 させたりしていけばよいわけです。

Makefileの例

先の例では、話を簡単にするために、テストスクリプトをコマンドラインから 直接指定して実行しましたが、実際にはもう少し楽をしたいところです。 テストの実行を効率化するためのMakefile(GNU Make)の例を書いておきます (行頭の空白は、適宜TAB文字に置き換えてください)。

#!/usr/bin/make

EMACS    = emacs --batch --quick --directory .
WGET     = wget --timestamping
SRC      = my-fold.el
cached   = ert.el
clean   += $(cached)
testlogs = $(foreach f,$(SRC),$(f:%.el=test-%-el.log))
mostlyclean += $(testlogs)

.PHONY: all test mostlyclean clean

all: test

test: $(testlogs)

test-%-el.log: test-%.el ert.el
        $(EMACS) --eval '(load-file "$<")' 2>&1 | tee $@

ert.el:
        $(WGET) --output-document=$@ \
        http://git.savannah.gnu.org/cgit/emacs.git/plain/lisp/emacs-lisp/ert.el

mostlyclean:
        -rm -f $(mostlyclean)

clean: mostlyclean
        -rm -f $(clean)

テスト対象のスクリプトは、SRCにスペース区切りで列挙してください。 テストスクリプトには、test-*.elというファイル名を付けておいてください。

make testでテストが実行され、メッセージが画面とログファイル(*.log) に出力されます。

$ make test

make mostlycleanでログファイルが消去されます。

$ make mostlyclean

なお、自動的にダウンロードされるert.elを消去したい場合は、 make cleanを実行してください。

覚え書き

試している途中でいくつか失敗をしたのでメモしておきます。

  • emacsの--scriptオプションは--directoryオプションよりも優先されるらしい。

    app.eld/lib.elがあって、app.el(require 'lib)しているとする。 このとき、d/lib.elを使うために

    $ emacs --directory d --script app.el
    

    とすると、 d/lib.elの読み込みに失敗してCannot open load file: libと 言われる。dload-pathに追加される前にapp.elが読まれるらしい。

    とりあえず--batch--evalに変更して回避した。

    $ emacs --batch --directory . --eval '(load-file "app.el")'
    
  • テストスクリプト内で、テスト対象よりも先にertをロードしないと エラーが起きることがあった。

参考資料