test forms and runner

Author: Chris Zheng  (z@caudate.me)
Date: 27 November 2018
Repository: https://github.com/zcaudate/hara
Version: 3.0.2

1    Introduction

hara.test is a test framework based off of the midje syntax, containing macros and helpers for easy testing and verification of functions

1.1    Installation

Add to project.clj dependencies:

[hara/test "3.0.2"]

All functions are in the hara.test namespace.

 (use (quote hara.test))

2    General



-main ^

main entry point for leiningen

v 3.0
(defn -main
  ([& args]
   (let [args (process-args args)
         {:keys [thrown failed] :as stats} (run :all)
         res (+ thrown failed)]
     (if (get args :exit)
       (System/exit res)
       res))))
link
(-main)

print-options ^

output options for test results

v 3.0
(defn print-options
  ([] (print-options :help))
  ([opts]
   (cond (set? opts)
         (alter-var-root #'common/*print*
                         (constantly opts))

         (= :help opts)
         #{:help :current :default :disable :all}

         (= :current opts) common/*print*

         (= :default opts)
         (alter-var-root #'common/*print* (constantly #{:print-thrown :print-failure :print-bulk}))

         (= :disable opts)
         (alter-var-root #'common/*print* (constantly #{}))

         (= :all opts)
         #{:print-thrown :print-success :print-facts :print-facts-success :print-failure :print-bulk})))
link
(print-options) => #{:disable :default :all :current :help} (print-options :default) => #{:print-bulk :print-failure :print-thrown}

run ^

runs all tests

v 3.0
(definvoke run
  [:task {:template :test
          :main {:fn executive/run}
          :params {:title "TESTING PROJECT"}}])
link
(run :list) (run 'hara.core.base.util) ;; {:files 1, :thrown 0, :facts 8, :checks 18, :passed 18, :failed 0} => map?

run-errored ^

runs only the tests that have errored

v 3.0
(defn run-errored
  []
  (let [latest @executive/+latest+]
    (-> (set/union (set (:errored latest))
                   (set (map (comp :ns :meta) (:failed latest)))
                   (set (map (comp :ns :meta) (:thrown latest))))
        (run))))
link
(run-errored)

3    Checker



all ^

checker that allows `and` composition of checkers

v 3.0
(defn all
  [& cks]
  (let [cks (mapv base/->checker cks)]
    (common/checker
     {:tag :all
      :doc "Checks if the result matches all of the checkers"
      :fn (fn [res]
            (->> cks
                 (map #(base/verify % res))
                 (every? base/succeeded?)))
      :expect cks})))
link
(mapv (all even? #(< 3 %)) [1 2 3 4 5]) => [false false false true false]

any ^

checker that allows `or` composition of checkers

v 3.0
(defn any
  [& cks]
  (let [cks (mapv base/->checker cks)]
    (common/checker
     {:tag :any
      :doc "Checks if the result matches any of the checkers"
      :fn (fn [res]
            (or (->> cks
                     (map #(base/verify % res))
                     (some base/succeeded?))
                false))
      :expect cks})))
link
(mapv (any even? 1) [1 2 3 4 5]) => [true true false true false]

anything ^

a checker that returns true for any value

v 3.0
(defn anything
  [x]
  ((satisfies util/T) x))
link
(anything nil) => true (anything [:hello :world]) => true

approx ^

checker that allows approximate verifications

v 3.0
(defn approx
  ([v]
   (approx v 0.001))
  ([v threshold]
   (common/checker
    {:tag :approx
     :doc "Checks if the result is approximately the given value"
     :fn (fn [res] (< (- v threshold) (result/->data res) (+ v threshold)))
     :expect v})))
link
((approx 1) 1.000001) => true ((approx 1) 1.1) => false ((approx 1 0.0000001) 1.001) => false

contains ^

checker for maps and vectors

v 3.0
(defn contains
  [x & modifiers]
  (cond (map? x)
        (contains-map x)

        (set? x)
        (contains-set x)

        (sequential? x)
        (contains-vector x (set modifiers))

        :else
        (throw (ex-info "Cannot create contains checker" {:input [x modifiers]}))))
link
((contains {:a odd? :b even?}) {:a 1 :b 4}) => true ((contains {:a 1 :b even?}) {:a 2 :b 4}) => false ((contains [1 2 3]) [1 2 3 4]) => true ((contains [1 3]) [1 2 3 4]) => false

contains-in ^

shorthand for checking nested maps and vectors

v 3.0
(defmacro contains-in
  [x]
  "A macro for nested checking of data in the `contains` form"
  (cond (map? x)
        `(contains ~(map/map-vals (fn [v] `(contains-in ~v)) x))
        (set? x)
        `(contains ~(mapv (fn [v] `(contains-in ~v)) x)
                   :in-any-order)

        (vector? x)
        `(contains ~(mapv (fn [v] `(contains-in ~v)) x))
        :else x))
link
((contains-in {:a {:b {:c odd?}}}) {:a {:b {:c 1 :d 2}}}) => true ((contains-in [odd? {:a {:b even?}}]) [3 {:a {:b 4 :c 5}}]) => true

exactly ^

checker that allows exact verifications

v 3.0
(defn exactly
  ([v]
   (exactly v identity))
  ([v function]
   (common/checker
    {:tag :exactly
     :doc "Checks if the result exactly satisfies the condition"
     :fn (fn [res] (= (function (result/->data res)) v))
     :expect v})))
link
((exactly 1) 1) => true ((exactly Long) 1) => false ((exactly number?) 1) => false

is-not ^

checker that allows negative composition of checkers

v 3.0
(defn is-not
  ([ck]
   (is-not ck identity))
  ([ck function]
   (let [ck (base/->checker ck)]
     (common/checker
      {:tag :is-not
       :doc "Checks if the result is not an outcome"
       :fn (fn [res]
             (not (ck (function res))))
       :expect ck}))))
link
(mapv (is-not even?) [1 2 3 4 5]) => [true false true false true]

just ^

combination checker for both maps and vectors

v 3.0
(defn just
  [x & modifiers]
  (cond (map? x)
        (just-map x)

        (set? x)
        (just-set x)

        (vector? x)
        (just-vector x (set modifiers))

        :else
        (throw (ex-info "Cannot create just checker" {:input [x modifiers]}))))
link
((just {:a odd? :b even?}) {:a 1 :b 4}) => true ((just {:a 1 :b even?}) {:a 1 :b 2 :c 3}) => false ((just [1 2 3 4]) [1 2 3 4]) => true ((just [1 2 3]) [1 2 3 4]) => false ((just [3 2 4 1] :in-any-order) [1 2 3 4]) => true

just-in ^

shorthand for exactly checking nested maps and vectors

v 3.0
(defmacro just-in
  [x]
  "A macro for nested checking of data in the `just` form"
  (cond (map? x)
        `(just ~(map/map-vals (fn [v] `(just-in ~v)) x))

        (set? x)
        `(just ~(mapv (fn [v] `(just-in ~v)) x)
               :in-any-order)

        (vector? x)
        `(just ~(mapv (fn [v] `(just-in ~v)) x))

        :else x))
link
((just-in {:a {:b {:c odd?}}}) {:a {:b {:c 1 :d 2}}}) => false ((just-in [odd? {:a {:b even?}}]) [3 {:a {:b 4}}]) ((just-in [odd? {:a {:b even?}}]) [3 {:a {:b 4}}]) => true

satisfies ^

checker that allows loose verifications

v 3.0
(defn satisfies
  ([v]
   (satisfies v identity))
  ([v function]
   (common/checker
    {:tag :satisfies
     :doc "Checks if the result can satisfy the condition:"
     :fn (fn [res]
           (let [data (function (result/->data res))]
             (cond (= data v) true
                   
                   (class? v) (instance? v data)

                   (and (check/comparable? v data)
                        (zero? (compare v data)))
                   true
                   
                   (map? v) (= (into {} data) v)

                   (vector? v) (= data v)

                   (ifn? v) (boolean (v data))
                   
                   (check/regexp? v)
                   (cond (check/regexp? data)
                         (= (.pattern ^Pattern v)
                            (.pattern ^Pattern data))

                         (string? data)
                         (boolean (re-find v data))

                         :else false)
                   
                   :else false)))
     :expect v})))
link
((satisfies 1) 1) => true ((satisfies Long) 1) => true ((satisfies number?) 1) => true ((satisfies #{1 2 3}) 1) => true ((satisfies [1 2 3]) 1) => false ((satisfies number?) "e") => false ((satisfies #"hello") #"hello") => true

throws ^

checker that determines if an exception has been thrown

v 3.0
(defn throws
  ([]  (throws Throwable))
  ([e] (throws e nil))
  ([e msg]
   (common/checker
    {:tag :throws
     :doc "Checks if an exception has been thrown"
     :fn (fn [{:keys [data status]}]
           (and (= :exception status)
                (instance? e data)
                (if msg
                  (= msg (.getMessage data))
                  true)))
     :expect {:exception e :message msg}})))
link
((throws Exception "Hello There") (result/map->Result {:status :exception :data (Exception. "Hello There")})) => true

throws-info ^

checker that determines if an `ex-info` has been thrown

v 3.0
(defn throws-info
  ([]  (throws-info {}))
  ([m]
   (common/checker
    {:tag :raises
     :doc "Checks if an issue has been raised"
     :fn (fn [{:keys [^Throwable data status]}]
           (and (= :exception status)
                (instance? clojure.lang.ExceptionInfo data)
                ((contains m) (ex-data data))))
     :expect {:exception clojure.lang.ExceptionInfo :data m}})))
link
((throws-info {:a "hello" :b "there"}) (common/evaluate {:form '(throw (ex-info "hello" {:a "hello" :b "there"}))})) => true

4    Walkthrough

4.1    Running

Tests can be run in the repl:

(use 'hara.test)

(run)

or in the shell:

> lein run -m hara.test :exit

4.2    Basics

For those that are familiar with midje, it's pretty much the same thing. We define tests in a fact expression:

(fact "lets test to see which if numbers are odd"

  1 => odd?

  2 => odd?)

;; Failure  [hara_test.clj]
;;    Info  "lets test to see which if numbers are odd"
;;    Form  2
;;   Check  odd?
;;  Actual  2

Or for the more grammatically correct, a facts expression:

(facts "lets test to see which if numbers are even?"

       1 => even?

       2 => even?)

;; Failure  [hara_test.clj]
;;    Info  "lets test to see which if numbers are even?"
;;    Form  1
;;   Check  even?
;;  Actual  1

The arrow => forms an input/output syntax and can only be in the top level fact form. This means that it cannot be nested arbitrarily in let forms as in midje. This has the effect simplifying the codebase as well as forcing each individual test to be more self-sufficient. The arrow is flexible and is designed so that the input/output syntax can be kept as succinct as possible. Other checks that can be performed are given as follows:

(facts "Random checks that show-off the range of the `=>` checker"

  ;; check for equality
       1 => 1

  ;; check for being in a set
       1 => #{1 2 3}

  ;; check for class
       1 => Long

  ;; check for function
       1 => number?

  ;; check for pattern
       "one" => #"ne")

4.3    Metadata

Metadata can be placed on the fact/facts form in order to provide more information as to what exactly the fact expression is checking:

^{:refer hara.test/fact :added "2.4" :tags #{:api}}
(fact "adding metadata gives more information"

  (+ 1 2 3) => (+ 3 3))

Metadata allows the test framework to quickly filter through what test are necessary, as well as to enable generation of documentation and docstrings through external tools.

4.4    Options

Options for run and run-namespace include:

  • specifing the :test-paths option (by default it is "test")
  • specifing :include and :exclude entries for file selection
  • specifing :check options:
    • :include and :exclude entries:
      • :tags so that only the :tags that are there on the meta data will run.
      • :refers can be a specific function or a namespace
      • :namespaces refers to specific test namespaces
  • specifing :print options for checks

Some examples can be seen below:

(run {:checks {:include [{:tags #{:web}}]} ;; only test for :web tags
      :test-paths ["test/hara"]}) ;; check out "test/hara" as the main path
=> {:files 0, :thrown 0, :facts 0, :checks 0, :passed 0, :failed 0}

Only test the hara.time-test namespace

(run {:checks {:include [{:namespaces #{'hara.time-test}}]}})
=> {:files 1, :thrown 0, :facts 32, :checks 53, :passed 53, :failed 0}

;; Summary (1)
;;   Files  1
;;   Facts  32
;;  Checks  53
;;  Passed  53
;;  Thrown  0
;;
;; Success (53)

Only test facts that refer to methods with hara.time namespace:

(run {:test-paths ["test/hara"]
      :checks {:include [{:refers '#{hara.time}}]}})
=> {:files 1, :thrown 0, :facts 32, :checks 53, :passed 53, :failed 0}
;; Summary (1)
;;   Files  1
;;   Facts  32
;;  Checks  53
;;  Passed  53
;;  Thrown  0
;;
;; Success (53)

Only pick one file to test, and suppress the final summary:

(run {:test-paths ["test/hara"]
      :include    ["^time"]
      :print      #{:print-facts}})
=> {:files 8, :thrown 0, :facts 54, :checks 127, :passed 127, :failed 0}
;;   Fact  [time_test.clj:9] - hara.time/representation?
;;   Info  "checks if an object implements the representation protocol"
;; Passed  2 of 2

;;   Fact  [time_test.clj:16] - hara.time/duration?
;;   Info  "checks if an object implements the duration protocol"
;; Passed  2 of 2

;; ...