Hello,

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

Attachment: smime.p7s
Description: S/MIME cryptographic signature

Reply via email to