It will certainly make writing tests more enjoyable :-)
Inspiration for :equal-pairs/each= came from the test framework I wrote
for newLISP:
http://newlisp-on-noodles.org/wiki/index.php/Function_Testing
Tests there are written as each= with 2 exceptions:
'->' evaluates the next expression without testing it
'err' works as 'throws'
Function 'test-fn' evaluates tests and returns the list of results.
Function 'print-test-fn' can take these results and just print them out.
Feel free to get inspired from it. If you see something nice, we can use
it.
Frantisek
PS: newLISP is changing fast. Test framework was written for older version
so many things might not work at this moment.
Few more ideas for you :)
each= could be extended to allow more freedom. The first parameter could
say what operation we want to perform:
each =
each not=
each <
each >
...
(each =
a b
c d)
=>
(all-true
(= a b)
(= c d))
(each not=
a b
c d)
=>
(all-true
(not= a b)
(not= c d))
Since 'each' might be too general (could it be part of Clojure core?), I
also like the name is-each which fits nicely with 'is'.
is-each =
is-each not=
...
It might be a good idea to check if count of forms passed to each/is-each
is even:
(if (odd? (count forms)) (throw ... "even number of forms needed!"))
Many thanks, Frantisek
Lets look at this test for minus:
(deftest test-minus
(all-true
(number? (- 1 2))
(integer? (- 1 2))
(float? (- 1.0 2))
(ratio? (- 2/3 1))
(float? (- 2/3 (/ 1.0 3))))
(throws IllegalArgumentException (-))
(each=
(- 1) -1
(- 1 2) -1
(- 1 2 3) -4
(- -2) 2
(- 1 -2) 3
(- 1 -2 -3) 6
(- 1 1) 0
(- -1 -1) 0
(- 2/3) -2/3
(- 2/3 1) -1/3
(- 2/3 1/3) 1/3
(- 1.2) -1.2
(- 2.2 1.1) 1.1
(- 6.6 2.2 1.1) 3.3)
; no underflow :)
(is (< (- Integer/MIN_VALUE 10) Integer/MIN_VALUE)))
There are currently 4 elements - all-true, throws, each=, is. I am
scratching my head, trying to figure out, how to merge these together or
to make it read better...
Ideas:
A) Lets merge 'is' and 'all-true'
Would it be possible to abandon the optional message 'is' macro has? I am
sure, I will not use it. Then 'is' could accept multiple params and we are
down to 3! (all-true is out)
B) What about 'throws' macro? Could this become a function returning
true/false? Then we could stick it inside 'is' or 'all-true'. (I guess it
doesn't matter that much, does it?)
C) Lets name them more similar, so it reads better: 'is', 'is-each',
'is-true'.
Just what came to my mind :-)
Greetings, Frantisek
Am 18.11.2008 um 00:13 schrieb Frantisek Sodomka:
> B) What about 'throws' macro? Could this become a function returning
> true/false? Then we could stick it inside 'is' or 'all-true'. (I
> guess it
> doesn't matter that much, does it?)
I have a is-like construct, which is build-up slightly different.
Instead of checking, what I get, I immediatelly dispatch to a
multimethod.
(defmacro is [t msg] (is* t msg))
(defmulti is* (fn [t _] (first t)))
(defmethod is* :default `(simply-run-t-here ...))
(defmethod is* nil `(always-fail ...))
(defmethod is* '= `(do-test-here ...))
(defmethod is* 'not= `(do-test-here ...))
(defmethod is* 'instance? `(do-test-here ...))
(defmethod is* 'throwing? `(do-test-here ...))
So it is easy to extend is with other tests, while still being able
to provide diagnostics in case a test should fail.
Furthermore I use a test driver function, which reduces the repition to
a minimum. It takes care to run the tests in a try, report the result
and provide diagnostics in case of a failure. Then each methods above
just has to specify in a callback how the actual test is carried out and
what kind diagnostics should be printed. (eg. compare assert-expr
for = and instance?. They are very similar.)
What do you think about such a structure?
Sincerely
Meikel
as previously threatened here the anatomy of my TAP library.
The testing and reporting is split in separate parts. The testing
part provides is as main interface. I first followed Perl's Test::More,
but thought it would be better to be closer to test-is. So I adopted
is, but - as I think - in slightly easier fashion.
The core is (as with test-is) the is macro, which is implemented
as a multimethod. Since there is no multimacro, we use is* for
the method.
(defmulti is* (fn [x & _] (if (seq? t) (first t) t)))
(defmacro is [t & desc] (is* t (first desc)))
Now we can start to define tests.
; (is (= actual expected) "description")
(defmethod is* '=
[t desc]
(let [actual (nth t 1)
exptd (nth t 2)]
`(test-driver (fn [] ~actual)
(quote ~actual)
(fn [] ~exptd)
~desc
(fn [e# a#] (= e# a#))
(fn [e# a# r#]
(diag (.concat "Expected: " a#))
(diag (.concat "to be: " e#))
(diag (.concat "but was: " r#))))))
Now this looks scary. So please let me explain. The first argument
packages about the "actual" expression. The second quotes it. The
third is the "expected" expression. It is also packaged up in a
closure. The fourth is the test description and the fifth the actual
test. The last argument is a callback, which is called in case the
test fails and which might be used to provide specialised diagnostic
message. I like having my tests tell me, why they failed.
While this seems quite complicated, I think it really is this essence
of a test. Everything else is boilerplate which is handled in the
test-driver function.
(defn test-driver
[actual qactual exp desc pred diagnose]
(try
(let [e (exp)
a (actual)
r (pred e a)]
(report-result r desc)
(when-not r
(let [es (pr-str e)
as (pr-str qactual)
rs (pr-str a)]
(diagnose es as rs)))
a)
(catch Exception e
(report-result false desc)
(diag (str "Exception was thrown: " e))
`test-failed)))
So this is pretty straightforward. Fire up a try, evaluate the
expected and actual expression, run the predicate and check the
result. In case the test fails or an Exception is thrown, the
failure is reported and some diagnostics are printed.
So that completes the testing part. From the user point of
view one has function - is -, which handles all the testing.
To provide a new test form, one simply defines a new method.
In the method, one simply passes the work to test-driver which
takes care for reporting and proper test execution.
The reporting side already showed up here and there in form of
the diag and the report-result functions. Others are plan (for
TAP), get-result to retrieve the test results. They act on a
global Var *the-harness*. They can also be implemented as
multimethods. A TAP harness would produce TAP output, an
"interactive" harness would only report failures and maybe some
statistics, a "batch" harness just keeps book of failing tests
and diagnostics for recursive use.
Changing a harness is a simple matter of
(binding [*the-harness* (make-some-harness)]
(do-some tests here))
Although this looks terribly complicated and is a hard to
digest bunch of stuff, I'd appreciate your comments. However,
how this can be made more functional... I have no clue.
Sincerely
Meikel