Search

Dark theme | Light theme
Showing posts with label ClojureGoodness:Testing. Show all posts
Showing posts with label ClojureGoodness:Testing. Show all posts

May 21, 2024

Clojure Goodness: Extending is Macro With Custom Assertions

The is macro in the clojure.test namespace can be used to write assertions about the code we want to test. Usually we provide a predicate function as argument to the is macro. The prediction function will call our code under test and return a boolean value. If the value is true the assertion passes, if it is false the assertion fails. But we can also provide a custom assertion function to the is macro. In the clojure.test package there are already some customer assertions like thrown? and instance?. The assertions are implemented by defining a method for the assert-expr multimethod that is used by the is macro. The assert-expr multimethod is defined in the clojure.test namespace. In our own code base we can define new methods for the assert-expr multimethod and provide our own custom assertions. This can be useful to make tests more readable and we can use a language in our tests that is close to the domain or naming we use in our code.

The implementation of the custom assertion should call the function do-report with a map containing the keys :type, :message, :expected and :actual. The :type key can have the values :fail or :pass. Based on the code we write in our assertion we can set the value correctly. Mostly the :message key will have the value of the message that is defined with the is macro in our tests. The keys :expected and :actual should contain reference to what the assertion expected and the actual result. This can be a technical reference, but we can also make it a human readable reference.

In the following example we implement a new customer assertion jedi? that checks if a given name is a Jedi name. The example is based on an example that can be found in the AssertJ documentation.

(ns mrhaki.test
  (:require [clojure.test :refer [deftest is are assert-expr]]))

(defmethod assert-expr 'jedi?
  "Assert that a given name is a Jedi."
  [msg form]
  `(let [;; We get the name that is the second element in the form.
         ;; The first element is the symbol `'jedi?`.
         name# ~(nth form 1)
         ;; We check if the name is part of a given set of Jedi names.
         result# (#{"Yoda" "Luke" "Obiwan"} name#)
         ;; We create an expected value that is used in the assertion message.
         expected# (str name# " to be a jedi.")]
     (if result#
       (do-report {:type     :pass
                   :message  ~msg,
                   :expected expected#
                   :actual   (str name# " is actually a jedi.")})
       (do-report {:type     :fail
                   :message  ~msg,
                   :expected expected#
                   :actual   (str name# " is NOT a jedi.")}))
     result#))

;; We can use our custom assertion in our tests.
(deftest jedi
  (is (jedi? "Yoda")))

;; The custom assertion can also be used with
;; the are macro as it will expand into multiple
;; is macro calls.
(deftest multiple-jedi
  (are [name] (jedi? name)
    "Yoda" "Luke" "Obiwan"))

;; The following test will fail, so we can
;; see failure message with the :expected and :actual values.
(deftest fail-jedi
  (is (jedi? "R2D2") "Is it?"))

If we run our failing test we see in the output that the assertion message is using our definition of the expected and actual values:

...
 expected: "R2D2 to be a jedi."
   actual: "R2D2 is NOT a jedi."
...

Written with Clojure 1.11.3.

May 19, 2024

Clojure Goodness: Combine Multiple Test Cases With are

The clojure.test namespace has the are macro that allows us to combine multiple test cases for the code we want to test, without having to write multiple assertions. We can provide multiple values for a function we want to test together with the expected values. Then then macro will expand this to multiple expressions with the is macro where the real assertion happens. Besides providing the data we must also provide the predicate where we assert our code under test. There is a downside of the are macro and that is that in case of assertion failures the line numbers in the error message could be off.

The first argument of the are macro is a vector with symbols that represent the names of the variables we want to use in the predicate. The second argument is the predicate where we write our assertion for the code under test. The remaining arguments are values for the symbols in the first argument. We can provide multiple sets of values for the symbols in the first argument, and the are macro will use this when the macro is expanded into multiple expressions.

In the next example we use the are macro to test the full-name function.

(ns mrhaki.test
  (:require [clojure.test :refer [deftest are]]
            [clojure.string :as str]))

;; Function we want to test.
(defn is-palidrome?
  [s]
  (= s (str/reverse s)))

(deftest palidrome
  (are
   ;; Vector with symbol used in test expression
   [s]
   ;; Test expression where we test the is-palidrome? function
   (true? (is-palidrome? s))
    ;; Data for the input symbol s
    "radar"
    "kayak"
    "racecar"
    "madam"
    "refer"
    "step on no pets"))

In the following example we use a test expression where we also use an expected value that is provided with the input data:

(ns mrhaki.test
  (:require [clojure.test :refer [deftest are]]))

;; Function we want to test.
(defn full-name
  "Returns a full name"
  [first-name last-name]
  (str first-name " " last-name))

(deftest sample
  (are
   [first-name last-name result]
   (= (full-name first-name last-name) result)
    "Fat" "Tony" "Fat Tony"
    "Krusty" "the Clown" "Krusty the Clown"))

Written with Clojure 1.11.3.