ユニットテストでダミーの関数に差し替える

Common Lispユニットテストを行う場合に、ユニットテストから呼び出した関数の内部で呼ばれる関数を本来の動作とは違う別の関数に差し替えたい場合がある。

関数を一時的に置き換える方法についてのメモ。

関数

例えば次の例では関数 charlie をテストしようと思った場合に、その中で呼び出される関数 alfa を差し替えたいとする。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(pprint (eq (charlie) :delta))

上記のコードは、charlie:bravo を返すため nil となる。

危険なバージョン

最低限の機能を示すための危険なバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(setf (symbol-function 'alfa) (lambda () :delta))
(pprint (eq (charlie) :delta))

上記のコードは、charlie:delta を返すため t となる。 最後から2行目で symbol-function への setf で、関数 alfacharlie 自体の変更をすることなく呼び出される関数 alfa を変更する事が出来ている。

多分安全なバージョン

ユニットテストで一時的に関数を変更したいということを考えると、最後は元々の関数に戻って欲しい。そのバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(let ((orig-func (symbol-function 'alfa)))
  (unwind-protect (progn
                    (setf (symbol-function 'alfa) (lambda () :delta))
                    (pprint (eq (charlie) :delta)))
    (setf (symbol-function 'alfa) orig-func)))
(pprint (eq (charlie) :delta))

最後から3行目のコードは関数 alfa が差し変わっているので t となり、最後の行は関数 alfa が元に戻っているので nil となる。 unwind-protect は内部でなんらかコンディションが発生した場合でも確実に元に戻せるように。

メソッド

(defclass delta () ())

(defmethod echo :before ((delta delta))
  (pprint :foxtrot))

(defmethod echo ((delta delta))
  (pprint :golf))

(echo (make-instance 'delta))

実行結果

:FOXTROT
:GOLF

クラスのメソッドを差し替える場合には、総称関数およびオブジェクトシステムに関連して一手間かける必要がある。 まず総称関数オブジェクトを取得し、それを使ってオブジェクトシステムからqualifierやspecifierの一致するメソッドを取得する。

一度beforeメソッドを削除して、元に戻す。

(defclass delta () ())

(defmethod echo :before ((d delta))
  (pprint :foxtrot))

(defmethod echo ((d delta))
  (pprint :golf))

(let ((d (make-instance 'delta))
       (orig-method (find-method #'echo  '(:before) (mapcar #'find-class '(delta)))))
  (remove-method #'echo orig-method)
  (echo d)
  (add-method #'echo orig-method)
  (echo d))

実行結果

:GOLF
:FOXTROT
:GOLF