1 ;;; el-mock.el --- Tiny Mock and Stub framework in Emacs Lisp
2 ;; $Id: el-mock.el,v 1.17 2008/08/28 19:04:48 rubikitch Exp $
4 ;; Copyright (C) 2008 rubikitch
6 ;; Author: rubikitch <rubikitch@ruby-lang.org>
7 ;; Keywords: lisp, testing, unittest
8 ;; URL: http://www.emacswiki.org/cgi-bin/wiki/download/el-mock.el
10 ;; This file is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; This file is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING. If not, write to
22 ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 ;; Boston, MA 02110-1301, USA.
27 ;; Emacs Lisp Mock is a library for mocking and stubbing using
28 ;; readable syntax. Most commonly Emacs Lisp Mock is used in
29 ;; conjunction with Emacs Lisp Expectations, but it can be used in
32 ;; Emacs Lisp Mock provides two scope interface of mock and stub:
33 ;; `with-mock' and `mocklet'. `with-mock' only defines a
34 ;; scope. `mocklet' is more sophisticated interface than `with-mock':
35 ;; `mocklet' defines local mock and stub like `let', `flet', and
38 ;; Within `with-mock' body (or argument function specified in
39 ;; `mock-protect'), you can create a mock and a stub. To create a
40 ;; stub, use `stub' macro. To create a mock, use `mock' macro.
42 ;; For further information: see docstrings.
43 ;; [EVAL IT] (describe-function 'with-mock)
44 ;; [EVAL IT] (describe-function 'mocklet)
45 ;; [EVAL IT] (describe-function 'stub)
46 ;; [EVAL IT] (describe-function 'mock)
50 ;; $Log: el-mock.el,v $
51 ;; Revision 1.17 2008/08/28 19:04:48 rubikitch
52 ;; Implement `not-called' mock.
54 ;; Revision 1.16 2008/08/28 18:23:28 rubikitch
55 ;; unit test: use dont-compile
57 ;; Revision 1.15 2008/04/18 18:02:24 rubikitch
58 ;; bug fix about symbol
60 ;; Revision 1.14 2008/04/13 18:23:43 rubikitch
61 ;; removed `message' advice.
62 ;; mock-suppress-redefinition-message: suppress by empty message
64 ;; Revision 1.13 2008/04/12 17:36:11 rubikitch
65 ;; raise mock-syntax-error when invalid `mock' and `stub' spec.
67 ;; Revision 1.12 2008/04/12 17:30:33 rubikitch
68 ;; inhibit using `mock' and `stub' outside `mock-protect' function.
70 ;; Revision 1.11 2008/04/12 17:10:42 rubikitch
71 ;; * added docstrings.
72 ;; * `stublet' is an alias of `mocklet'.
74 ;; Revision 1.10 2008/04/12 16:14:16 rubikitch
75 ;; * allow omission of return value
76 ;; * (mock foo 2) and (stub foo 2) cause error now
79 ;; Revision 1.9 2008/04/12 15:10:32 rubikitch
80 ;; changed mocklet syntax
82 ;; Revision 1.8 2008/04/12 14:54:16 rubikitch
85 ;; Revision 1.7 2008/04/10 16:14:02 rubikitch
86 ;; fixed advice-related bug
88 ;; Revision 1.6 2008/04/10 14:08:32 rubikitch
89 ;; *** empty log message ***
91 ;; Revision 1.5 2008/04/10 14:01:48 rubikitch
94 ;; Revision 1.4 2008/04/10 12:57:00 rubikitch
97 ;; Revision 1.3 2008/04/10 07:50:10 rubikitch
98 ;; *** empty log message ***
100 ;; Revision 1.2 2008/04/10 07:48:04 rubikitch
106 ;; refactored with-stub-function
108 ;; Revision 1.1 2008/04/10 07:37:54 rubikitch
114 (eval-when-compile (require 'cl
))
117 ;;;; stub setup/teardown
118 (defun stub/setup
(funcsym value
)
119 (mock-suppress-redefinition-message
121 (when (fboundp funcsym
)
122 (put 'mock-original-func funcsym
(symbol-function funcsym
)))
123 (ad-safe-fset funcsym
`(lambda (&rest x
) ,value
)))))
125 (defun stub/teardown
(funcsym)
126 (mock-suppress-redefinition-message
128 (let ((func (get 'mock-original-func funcsym
)))
130 (fmakunbound funcsym
)
131 (ad-safe-fset funcsym func
)
135 ;;;; mock setup/teardown
136 (defun mock/setup
(func-spec value
)
137 (mock-suppress-redefinition-message
139 (let ((funcsym (car func-spec
)))
140 (when (fboundp funcsym
)
141 (put 'mock-original-func funcsym
(symbol-function funcsym
)))
142 (put 'mock-not-yet-called funcsym t
)
143 (ad-safe-fset funcsym
144 `(lambda (&rest actual-args
)
145 (put 'mock-not-yet-called
',funcsym nil
)
146 (add-to-list 'mock-verify-list
147 (list ',funcsym
',(cdr func-spec
) actual-args
))
150 (defun not-called/setup
(funcsym)
151 (mock-suppress-redefinition-message
154 (when (fboundp funcsym
)
155 (put 'mock-original-func funcsym
(symbol-function funcsym
)))
156 (ad-safe-fset funcsym
157 `(lambda (&rest actual-args
)
158 (signal 'mock-error
'(called))))))))
160 (defalias 'mock
/teardown
'stub
/teardown
)
163 (put 'mock-error
'error-conditions
'(mock-error error
))
164 (put 'mock-error
'error-message
"Mock error")
165 (defun mock-verify ()
166 (when (loop for f in -mocked-functions
167 thereis
(get 'mock-not-yet-called f
))
168 (signal 'mock-error
'(not-called)))
169 (loop for
(funcsym expected-args actual-args
) in mock-verify-list
171 (mock-verify-args funcsym expected-args actual-args
)))
173 (defun mock-verify-args (funcsym expected-args actual-args
)
174 (loop for e in expected-args
177 (unless (eq e
'*) ; `*' is wildcard argument
178 (unless (equal (eval e
) a
)
179 (signal 'mock-error
(list (cons funcsym expected-args
)
180 (cons funcsym actual-args
)))))))
181 ;;;; stub/mock provider
182 (defvar -stubbed-functions nil
)
183 (defvar -mocked-functions nil
)
184 (defvar mock-verify-list nil
)
185 (defvar in-mocking nil
)
186 (defun mock-protect (body-fn)
187 "The substance of `with-mock' macro.
188 Prepare for mock/stub, call BODY-FN, and teardown mock/stub.
191 When you adapt Emacs Lisp Mock to a testing framework, wrap test method around this function."
192 (let (mock-verify-list
196 (setplist 'mock-original-func nil
)
197 (setplist 'mock-not-yet-called nil
)
200 (mapcar #'stub
/teardown -stubbed-functions
)
203 (mapcar #'mock
/teardown -mocked-functions
)))))
206 (defun mock-suppress-redefinition-message (func)
207 "Erase \"ad-handle-definition: `%s' got redefined\" message."
211 (put 'mock-syntax-error
'error-conditions
'(mock-syntax-error error
))
212 (put 'mock-syntax-error
'error-message
"Mock syntax error")
215 (defmacro with-mock
(&rest body
)
216 "Execute the forms in BODY. You can use `mock' and `stub' in BODY.
217 The value returned is the value of the last form in BODY.
218 After executing BODY, mocks and stubs are guaranteed to be released.
227 (defalias 'with-stub
'with-mock
)
229 (defmacro stub
(function &rest rest
)
230 "Create a stub for FUNCTION.
231 Stubs are temporary functions which accept any arguments and return constant value.
232 Stubs are removed outside `with-mock' (`with-stub' is an alias) and `mocklet'.
236 Create a FUNCTION stub which returns nil.
237 * (stub FUNCTION => RETURN-VALUE)
238 Create a FUNCTION stub which returns RETURN-VALUE.
245 (and (null (foo)) (= (bar 7) 1))) ; => t
247 (let ((value (cond ((eq '=> (car rest
))
250 (t (signal 'mock-syntax-error
'("Use `(stub FUNC)' or `(stub FUNC => RETURN-VALUE)'"))))))
251 `(if (not in-mocking
)
252 (error "Do not use `stub' outside")
253 (stub/setup
',function
',value
)
254 (push ',function -stubbed-functions
))))
256 (defmacro mock
(func-spec &rest rest
)
257 "Create a mock for function described by FUNC-SPEC.
258 Mocks are temporary functions which accept specified arguments and return constant value.
259 If mocked functions are not called or called by different arguments, an `mock-error' occurs.
260 Mocks are removed outside `with-mock' and `mocklet'.
263 * (mock (FUNCTION ARGS...))
264 Create a FUNCTION mock which returns nil.
265 * (mock (FUNCTION ARGS...) => RETURN-VALUE)
266 Create a FUNCTION mock which returns RETURN-VALUE.
269 The `*' is a special symbol: it accepts any value for that argument position.
275 (and (= (f 9 2) 3) (null (g 3)))) ; => t
278 (g 7)) ; (mock-error (g 3) (g 7))
280 (let ((value (cond ((eq '=> (car rest
))
283 (t (signal 'mock-syntax-error
'("Use `(mock FUNC-SPEC)' or `(mock FUNC-SPEC => RETURN-VALUE)'"))))))
284 `(if (not in-mocking
)
285 (error "Do not use `mock' outside")
286 (mock/setup
',func-spec
',value
)
287 (push ',(car func-spec
) -mocked-functions
))))
289 (defmacro not-called
(function)
290 "Create a not-called mock for FUNCTION.
291 Not-called mocks are temporary functions which raises an error when called.
292 If not-called functions are called, an `mock-error' occurs.
293 Not-called mocks are removed outside `with-mock' and `mocklet'.
296 * (not-called FUNCTION)
297 Create a FUNCTION not-called mock.
305 (g 7)) ; => (mock-error called)
308 `(if (not in-mocking
)
309 (error "Do not use `not-called' outside")
310 (not-called/setup
',function
)
311 (push ',function -mocked-functions
))))
314 (defun mock-parse-spec (spec)
316 (mapcar (lambda (args)
317 (if (eq (cadr args
) 'not-called
)
318 `(not-called ,(car args
))
319 (cons (if (consp (car args
)) 'mock
'stub
)
323 (defun mocklet-function (spec body-func
)
325 (eval (mock-parse-spec spec
))
326 (funcall body-func
)))
328 (defmacro mocklet
(speclist &rest body
)
329 "`let'-like interface of `with-mock', `mock', `not-called' and `stub'.
331 Create mocks and stubs described by SPECLIST then execute the forms in BODY.
332 SPECLIST is a list of mock/not-called/stub spec.
333 The value returned is the value of the last form in BODY.
334 After executing BODY, mocks and stubs are guaranteed to be released.
337 Spec is arguments of `mock', `not-called' or `stub'.
338 * ((FUNCTION ARGS...)) : mock which returns nil
339 * ((FUNCTION ARGS...) => RETURN-VALUE) ; mock which returns RETURN-VALUE
340 * (FUNCTION) : stub which returns nil
341 * (FUNCTION => RETURN-VALUE) ; stub which returns RETURN-VALUE
342 * (FUNCTION not-called) ; not-called FUNCTION
345 (mocklet (((mock-nil 1))
349 (and (null (mock-nil 1)) (= (mock-1 4) 1)
350 (null (stub-nil 'any)) (= (stub-2) 2))) ; => t
352 `(mocklet-function ',speclist
(lambda () ,@body
)))
354 (defalias 'stublet
'mocklet
)
356 (put 'with-mock
'lisp-indent-function
0)
357 (put 'with-stub
'lisp-indent-function
0)
358 (put 'mocklet
'lisp-indent-function
1)
359 (put 'stublet
'lisp-indent-function
1)
363 (when (fboundp 'expectations
)
365 (desc "stub setup/teardown")
370 (stub/teardown
'foo
)))
374 (stub/teardown
'foox
)
376 (desc "with-mock interface")
396 (stub hoge
) ;omission of return value
404 (stub me
=> (+ 3 31))
406 ;; TODO defie mock-syntax-error / detect mock-syntax-error in expectations
407 (desc "abused stub macro")
408 (expect (error mock-syntax-error
'("Use `(stub FUNC)' or `(stub FUNC => RETURN-VALUE)'"))
411 (expect (error-message "Do not use `stub' outside")
412 (let (in-mocking) ; while executing `expect', `in-mocking' is t.
423 (+ (foo 5) (bar 7))))
425 (flet ((plus () (+ (foo 5) (bar 7))))
436 (mock (f * (1+ 1)) => (+ 0 1)) ;evaluated
440 (mock (f 2)) ;omission of return value
444 (mock (me 1) => 'hoge
)
448 (mock (me 1) => (+ 3 31))
451 (desc "unfulfilled mock")
452 (expect (error mock-error
'((foom 5) (foom 6)))
456 (expect (error mock-error
'((bar 7) (bar 8)))
460 (+ (foo 5) (bar 8))))
461 (expect (error mock-error
'(not-called))
463 (mock (foo 5) => 2)))
464 (expect (error mock-error
'(not-called))
469 (expect (error mock-error
'((f 2) (f 4)))
471 (mock (f 2)) ;omission of return value
473 (desc "abused mock macro")
474 (expect (error mock-syntax-error
'("Use `(mock FUNC-SPEC)' or `(mock FUNC-SPEC => RETURN-VALUE)'"))
477 (expect (error-message "Do not use `mock' outside")
478 (let (in-mocking) ; while executing `expect', `in-mocking' is t.
481 (desc "mock with stub")
486 (+ (f 1 2) (hoge 'a
))))
487 (expect (error mock-error
'((f 1 2) (f 3 4)))
491 (+ (f 3 4) (hoge 'a
))))
493 (desc "with-stub is an alias of with-mock")
495 (symbol-function 'with-stub
))
497 (desc "stublet is an alias of mocklet")
499 (symbol-function 'stublet
))
501 (desc "mock-parse-spec")
515 (mocklet (((f 1 2) => 3)
517 (+ (f 1 2) (hoge 'a
))))
519 (mocklet ((foo => 2))
522 (defun defined-func (x) 3)
524 (mocklet ((defined-func => 3))
526 (fmakunbound 'defined-func
)))
528 (mocklet ((f)) ;omission of return value
531 (mocklet (((f 76))) ;omission of return value
539 (desc "stub for defined function")
541 (defun blah (x) (* x
2))
543 (let ((orig (symbol-function 'blah
)))
544 (mocklet ((blah => "xxx"))
546 (fmakunbound 'blah
)))
548 (defun blah (x) (* x
2))
550 (let ((orig (symbol-function 'blah
)))
551 (mocklet ((blah => "xx"))
553 (equal orig
(symbol-function 'blah
)))
554 (fmakunbound 'blah
)))
556 (desc "stub for adviced function")
558 (mock-suppress-redefinition-message ;silence redefinition warning
560 (defun fugaga (x) (* x
2))
561 (defadvice fugaga
(around test activate
)
562 (setq ad-return-value
(concat "[" ad-return-value
"]")))
564 (let ((orig (symbol-function 'fugaga
)))
565 (mocklet ((fugaga => "xxx"))
567 (fmakunbound 'fugaga
)))))
569 (mock-suppress-redefinition-message
571 (defun fugaga (x) (* x
2))
572 (defadvice fugaga
(around test activate
)
573 (setq ad-return-value
(concat "[" ad-return-value
"]")))
575 (let ((orig (symbol-function 'fugaga
)))
576 (mocklet ((fugaga => "xx"))
578 (equal orig
(symbol-function 'fugaga
)))
579 (fmakunbound 'fugaga
)))))
581 (desc "mock for adviced function")
583 (mock-suppress-redefinition-message
585 (defun fugaga (x) (* x
2))
586 (defadvice fugaga
(around test activate
)
587 (setq ad-return-value
(concat "[" ad-return-value
"]")))
589 (let ((orig (symbol-function 'fugaga
)))
590 (mocklet (((fugaga "aaaaa") => "xx"))
592 (fmakunbound 'fugaga
)))))
594 (mock-suppress-redefinition-message
596 (defun fugaga (x) (* x
2))
597 (defadvice fugaga
(around test activate
)
598 (setq ad-return-value
(concat "[" ad-return-value
"]")))
600 (let ((orig (symbol-function 'fugaga
)))
601 (mocklet (((fugaga "aaaaa") => "xx"))
603 (equal orig
(symbol-function 'fugaga
)))
604 (fmakunbound 'fugaga
)))))
605 (desc "not-called macro")
610 (desc "mocklet/notcalled")
612 (mocklet ((foom not-called
))
614 (desc "unfulfilled not-called")
615 (expect (error mock-error
'(called))
619 (desc "abused not-called macro")
620 (expect (error-message "Do not use `not-called' outside")
621 (let (in-mocking) ; while executing `expect', `in-mocking' is t.
622 (not-called hahahaha
)))
623 (desc "not-called for adviced function")
625 (mock-suppress-redefinition-message ;silence redefinition warning
627 (defun fugaga (x) (* x
2))
628 (defadvice fugaga
(around test activate
)
629 (setq ad-return-value
(concat "[" ad-return-value
"]")))
631 (let ((orig (symbol-function 'fugaga
)))
632 (mocklet ((fugaga not-called
))
634 (fmakunbound 'fugaga
)))))
636 (mock-suppress-redefinition-message
638 (defun fugaga (x) (* x
2))
639 (defadvice fugaga
(around test activate
)
640 (setq ad-return-value
(concat "[" ad-return-value
"]")))
642 (let ((orig (symbol-function 'fugaga
)))
643 (mocklet ((fugaga not-called
))
645 (equal orig
(symbol-function 'fugaga
)))
646 (fmakunbound 'fugaga
)))))
653 ;; How to save (DO NOT REMOVE!!)
654 ;; (emacswiki-post "el-mock.el")
655 ;;; el-mock.el ends here