X-Git-Url: http://repo.macrolet.net/gitweb/?a=blobdiff_plain;f=README.md;h=5cbf824dac99ae3fa7812e8e3a78a85b871e9999;hb=HEAD;hp=b2a25438d22167346aa7a8eceb02b14765035713;hpb=1827ce6ddda5f48fc5d7ba40dac5bdaebff4a1b1;p=cl-mock.git diff --git a/README.md b/README.md index b2a2543..5cbf824 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,204 @@ -CL-MOCK - Mocking (generic) functions. +-*- mode: markdown; coding: utf-8-unix; -*- -Copyright (C) 2013 Olof-Joachim Frahm +CL-MOCK - Mocking functions. + +Copyright (C) 2013-15 Olof-Joachim Frahm Release under a Simplified BSD license. Working, but unfinished. -Should be portable thanks to [`CLOSER-MOP`][1]. +[![Build Status](https://travis-ci.org/Ferada/cl-mock.svg?branch=master)](https://travis-ci.org/Ferada/cl-mock) + +Portable to at least ABCL, Allegro CL (with one problem with inlining +settings), SBCL, CCL and CLISP. CMUCL possibly, but not tested on +Travis CI. ECL fails on Travis CI ([`OPTIMA`][3] fails there as well), +but runs successfully on my own machine, so YMMV. See the detailed +reports at +[https://travis-ci.org/Ferada/cl-mock](https://travis-ci.org/Ferada/cl-mock) +for more information and [`CL-TRAVIS`][5], and [`.travis.yml`][6] for the +setup. + + +# INTRODUCTION + +This small library provides a way to replace the actual implementation +of either regular or generic functions with mocks. On the one hand how +to integrate this facility with a testing library is up to the user; the +tests for the library are written in [`FIVEAM`][2] though, so most +examples will take that into account. On the other hand writing +interactions for mocks usually relies on a bit of pattern matching, +therefore the regular `CL-MOCK` package relies on [`OPTIMA`][3] to +provide that facility instead of deferring to the user. Should this be +a concern a reduced system definition is available as `CL-MOCK-BASIC`, +which excludes the definition of `ANSWER` and the dependency on +[`OPTIMA`][3]. Since it is pretty easy to just roll something like this on your own, -the main purpose is to develop a nice syntax (lispy, declarative) to -keep tests readable and maintainable. +the main purpose is to develop a nice (lispy, declarative) syntax to +keep your tests readable and maintainable. Some parts may be used independently of the testing facilities, -e.g. dynamic `FLET` and method bindings with `PROGM` may be of general -interest. +e.g. dynamic `FLET` may be of general interest. + + +# MOCKING REGULAR FUNCTIONS + +Let's say we have a function `FOO`, then we can replace it for testing +by establishing a new mocking context and then specifying how the new +function should behave (see below in **UTILITIES** for a more primitive +dynamic function rebinding): + + > (declaim (notinline foo bar)) + > (defun foo () 'foo) + > (defun bar (&rest args) + > (declare (ignore args)) + > 'bar) + > (with-mocks () + > (answer (foo 1) 42) + > (answer foo 23) + > (values + > (eql 42 (foo 1)) + > (eql 23 (foo 'bar)))) + > => T T + +The `ANSWER` macro has pattern matching (see [`OPTIMA`][3]) integrated. +Therefore something like the following will now work as expected: + + > (with-mocks () + > (answer (foo x) (format T "Hello, ~A!" x)) + > (foo "world")) + > => "Hello, world!" + +If you don't like `ANSWER` as it is, you can still use `IF-CALLED` +directly. Note however that unless `UNHANDLED` is called, the function +always matches and the return value is directly returned again: + + > (with-mocks () + > (if-called 'foo (lambda (x) + > (unhandled) + > (error "Not executed!"))) + > (if-called 'foo (lambda (x) (format T "Hello, ~A!" x))) + > (foo "world")) + > => "Hello, world!" + +Be especially careful to handle all given arguments, otherwise the +function call will fail and that error is propagated upwards. + +`IF-CALLED` also has another option to push a binding to the front of +the list, which (as of now) isn't available via `ANSWER` (and should be +treated as subject to change anyway). + +Should you wish to run the previously defined function, use the function +`CALL-PREVIOUS`. If no arguments are passed it will use the current +arguments from `*ARGUMENTS*`, if any. Otherwise it will be called with +the passed arguments instead. For cases where explicitely calling it +with no arguments is necessary, using `(funcall *previous*)` is still +possible as well. + + > (with-mocks () + > (answer foo `(was originally ,(funcall *previous*))) + > (answer bar `(was originally ,(call-previous))) + > (values + > (foo "hello") + > (bar "hello"))) + > => (WAS ORIGINALLY FOO) (WAS ORIGINALLY BAR) + +The function `INVOCATIONS` may be used to retrieve all recorded +invocations of mocks (so far); the optional argument can be used to +filter for a particular name: + + > (with-mocks () + > (answer foo) + > (foo "hello") + > (foo "world") + > (bar "test") + > (invocations 'foo)) + > => ((FOO "hello") + > (FOO "world")) + +Currently there are no further predicates to check these values, this is +however an area of investigation, so presumably either a macro like +[`FIVEAM`][2]s `IS`, or regular predicates could appear in this place. + + +# EXAMPLES + +The following examples may give a better impression. + +Here we test a particular [`ECLASTIC`][4] method, `GET*`. In order to +replace the HTTP call with a supplied value, we use `ANSWER` with +`HTTP-REQUEST` and return a pre-filled stream. Afterwards both the +number of `INVOCATIONS` and the actual returned values are checked. + + (use-package '(#:cl-mock #:fiveam #:eclastic #:drakma #:puri)) + + (def-test search.empty () + (let* ((events (make-instance ' :type "document" :index "index" + :host "localhost" :port 9292)) + (text "{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":5,\ + \"successful\":5,\"failed\":0},\"hits\":{\"total\":123,\"max_score\":1.0,\ + \"hits\":[{\"_index\":\"index\",\"_type\":\"document\",\"_id\":\"12345\",\ + \"_score\":1.0,\"_source\":{\"test\": \"Hello, World!\"}}]}}") + (stream (make-string-input-stream text))) + (with-mocks () + (answer http-request + (values stream 200 NIL + (parse-uri "http://localhost:9292/index/document/_search") + stream NIL "OK")) + (let ((values (multiple-value-list + (get* events (new-search NIL))))) + (is (eql 1 (length (invocations)))) + (is (eql 1 (length (car values)))) + (is-true (typep (caar values) ')) + (is (equal (cdr values) + '(NIL (:hits 123 + :shards (:total 5 :failed 0 :successful 5) + :timed-out NIL :took 3)))))))) + +Of course, running this should produce no errors: + + > (run! 'search.empty) + > + > Running test SEARCH.EMPTY .... + > Did 4 checks. + > Pass: 4 (100%) + > Skip: 0 ( 0%) + > Fail: 0 ( 0%) + > + > => NIL + + +# UTILITIES + +`DFLET` dynamically rebinds functions similar to `FLET`: + + > (defun foo () 42) + > (defun bar () (foo)) + > (bar) + > => 42 + > (dflet ((foo () 23)) + > (bar)) + > => 23 + > (OR) => 42, if FOO was inlined + +The caveat is that this might not work on certain optimisation settings, +including inlining. That trade-off seems acceptable; it would be nice +if a warning could be issued depending on the current optimisation +settings, however that is particularly implementation dependent, so lack +of a warning won't indicate a working environment. + +The underlying function `PROGF` may be used as well similarly to the +standard `PROG`: + + > (progf '(foo) (list (lambda () 23)) + > (bar)) + > => 23 + > (OR) => 42, if FOO was inlined [1]: http://common-lisp.net/project/closer/closer-mop.html +[2]: http://common-lisp.net/project/fiveam/ +[3]: https://github.com/m2ym/optima +[4]: https://github.com/gschjetne/eclastic +[5]: https://github.com/luismbo/cl-travis +[6]: https://raw.githubusercontent.com/Ferada/cl-mock/master/.travis.yml