This library provides an expressive alternative to clojure.test
, based on the syntax of Jay Fields' Expectations library, but fully compatible with all the clojure.test
-based tooling out there: no special plugins or editor modes are needed.
You can add expectations.clojure.test
to your project with either:
;; add this to :extra-deps under a :test alias:
com.github.seancorfield/expectations {:mvn/version "2.1.201"}
for deps.edn
or:
;; add this to :dev-dependencies (Leiningen)
[com.github.seancorfield/expectations "2.1.201"]
;; or add this to :dependencies (Boot)
[com.github.seancorfield/expectations "2.1.201" :scope "test"]
for project.clj
or build.boot
.
Then in your test namespaces, you just require expectations.clojure.test
(instead of clojure.test
) and start using the Expectations-style syntax for your tests.
This library is designed to work with Clojure 1.9 or later, and provides support for clojure.spec
predicates. It is also designed to work with Paul Stadig's Humane Test Output, which provides better failure messages for clojure.test
.
If you have pjstadig/humane-test-output
as a dependency (i.e., it is on your classpath), then when you require expectations.clojure.test
it will automatically activate Humane Test Output, regardless of how you are running your tests: again, no need for any special setup or :injections
(Leiningen).
Take note of the caveat Paul Stadig provides about some tooling and/or IDEs installing their own "helpers" for
clojure.test
output!
This example provides a quick comparison with clojure.test
(the tests match those in the clojure.test
documentation):
(require '[expectations.clojure.test
:refer [defexpect expect expecting run-tests test-vars]])
(defexpect simple-test ; (deftest simple-test
(expect 4 (+ 2 2)) ; (is (= 4 (+ 2 2)))
(expect Long 256) ; (is (instance? Long 256))
(expect (.startsWith "abcde" "ab")) ; (is (.startsWith "abcde" "ab"))
(expect ArithmeticException (/ 1 0)) ; (is (thrown? ArithmeticException (/ 1 0)))
(expecting "Arithmetic" ; (testing "Arithmetic"
(expecting "with positive integers" ; (testing "with positive integers"
(expect 4 (+ 2 2)) ; (is (= 4 (+ 2 2)))
(expect 7 (+ 3 4))) ; (is (= 7 (+ 3 4))))
(expecting "with negative integers" ; (testing "with negative integers"
(expect -4 (+ -2 -2)) ; (is (= -4 (+ -2 -2)))
(expect -1 (+ 3 -4))))) ; (is (= -1 (+ 3 -4))))))
The third example could also be written as follows, since expect
allows an arbitrary predicate in the "expected" position:
(expect #(.startsWith % "ab") "abcde")
Or like this, since expect
allows a regular expression in the "expected" position:
(expect #"^ab" "abcde")
Both of these more accurately reflect an expectation on the actual value "abcde"
, that the string begins with "ab"
, than the is
equivalent which has the actual value embedded in the test expression. Separating the "expectation" (value or predicate) from the "actual" expression being tested often makes the test much clearer.
How you run tests will depend a lot on the tooling and/or IDE/editor that you use in your day-to-day workflow.
While you are developing tests, it's probably best to run them via your editor (using the REPL connected to it). Most Clojure integrations for editors allow you run an individual test, all tests in a given namespace, or all tests in the project. You'll have to consult the documentation for your chosen editor/integration for the ways to do that.
If you are working directly in the REPL (not recommended but, hey...) you can run an individual test simply by calling it, as if it were a function:
user=> (simple-test)
nil
It will return nil
and print nothing if the test succeeds. It will print out failure messages otherwise (and still return nil
). While this is the simplest way to run a test, it is not always the best, since it won't run any test fixtures -- see Fixtures for more details. You can run a test (with fixtures) like this:
user=> (test-vars [#'simple-test])
nil
As you might imagine, you can run more than one test using test-vars
. You can also run all the tests in the current namespace, which produces more informative output:
user=> (run-tests)
Testing user
Ran 1 tests containing 8 assertions.
0 failures, 0 errors.
{:test 1, :pass 8, :fail 0, :error 0, :type :summary}
As of 2.0.0, test-vars
and run-tests
are imported from clojure.test
automatically behind the scenes, along with other test running functions.
This assumes you are using the CLI and deps.edn
for your project, and that you have set up a :test
alias per test-runner
's README:
> clojure -X:test
The following is usually sufficient to run tests via Leiningen, assuming your project.clj
file is set up correctly:
> lein test
The following is usually sufficient to run tests via Boot, assuming your build.boot
file is set up correctly (including Adzerk's boot-test
):
> boot test
While not directly related to how to run your tests, it's a common question asked by folks new to Clojure: where should I put my tests?
Most of the clojure.test
-based tooling assumes that for each source file src/path/to/my_code.clj
(which represents the namespace path.to.my-code
), you will have a test file test/path/to/my_code_test.clj
with the namespace path.to.my-code-test
.
That test file will generally start out with:
(ns path.to.my-code-test
(:require [expectations.clojure.test :refer [defexpect expect expecting ,,,]
[path.to.my-code :refer [the-functions you-want to-test]]]))
Following this convention means that all the tooling and IDE/editor integrations should work with no configuration: it's what everyone "expects".
clojure.test
has a macro called with-test
that allows you to define tests inline following your function definition. Given that clojure.test
ships directly with Clojure, this is reasonable because putting test code in your function definition's metadata doesn't add any dependencies and it has the benefit of being able to see the source of the function and the source of its test right next to each other. You can do that with Expectations too, since it is clojure.test
-compatible, although it does mean your source code has an additional dependency -- but Expectations is fairly small (~300 lines) and has no additional dependencies. As of 2.0.0, with-test
is available directly in expectations.clojure.test
.
However, if you put tests in your source files, using with-test
, then most tooling won't know how to find those tests by default. Here's an example of an inline test and how to run it with Leiningen and the CLI (deps.edn
):
(ns my.cool.project
(:require [expectations.clojure.test :refer [expect with-test]]))
(with-test
(defn square [x] (* x x))
(expect 1 (square 1))
(expect 1 (square -1))
(expect 100 (square 10)))
For Leiningen, you'll need to tell it to look for tests in src
(as well as test
) so add this to project.clj
:
:test-paths ["src" "test"]
then you can just run lein test
and it will check for tests inside the src
test, find my.cool.project/square
test metadata and run it as a test.
For the clojure
CLI, you'll need to tell Cognitect's test-runner
to look for tests in src
and you'll have to override it's default regex pattern for matching test namespaces:
clojure -X:test :dirs '["src"]' :patterns '[".*"]'
Of course, you can also update the :test
alias to add those new options into :exec-args
so that you don't need them on the command line:
{:aliases
{:test
{:extra-paths ["test"]
:extra-deps
{com.github.seancorfield/expectations {:mvn/version "2.1.201"}
;; assumes Clojure CLI 1.10.3.933 or later:
io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "48c3c67"}}
:exec-fn cognitect.test-runner.api/test
:exec-args {:dirs ["src" "test"]
:patterns [".*"]}}}}
Note that you'll need both src
and test
directories if you want test-runner
to look in both places.
If you are using Clojure 1.9 or later, you have access to Spec and can expect
those as well:
(require '[clojure.spec.alpha :as s])
(s/def :small/value (s/and pos-int? #(< % 100)))
(defexpect spec-test
(expect :small/value (* 14 3)))
If an expectation on a Spec fails, you get the explanation as well as the standard clojure.test
failure:
(defexpect spec-failure
(expect :small/value (* 14 30)))
;; when run:
FAIL in (spec-failure) (...:...)
(* 14 30)
val: 420 fails spec: :small/value predicate: (< % 100)
expected: (s/valid? :small/value (* 14 30))
actual: (not (s/valid? :small/value 420))
Just like the is
macro, the expect
macro can take an additional (third) argument that is a message to display if the expectation fails:
user=> (defexpect failure-msg
(expect even? (+ 1 1 1) "It's uneven!"))
#'user/failure-msg
user=> (failure-msg)
FAIL in (failure-msg) (...:...)
It's uneven!
(+ 1 1 1)
(+ 1 1 1) did not satisfy even?
expected: (even? (+ 1 1 1))
actual: (not (even? 3))
nil
;; messages are combined in a Spec failure:
user=> (defexpect spec-failure-msg
(expect :small/value (* 14 30) "Too big!"))
#'user/spec-failure-msg
user=> (spec-failure-msg)
FAIL in (spec-failure) (...:...)
Too big!
(* 14 30)
val: 420 fails spec: :small/value predicate: (< % 100)
expected: (s/valid? :small/value (* 14 30))
actual: (not (s/valid? :small/value 420))
nil
;; expecting adds its message too:
user=> (defexpect another-spec-failure-msg
(expecting "Large number should fail"
(expect :small/value (* 14 30) "Too big!"))
(expecting "Negative number should fail"
(expect :small/value (* -14 30) "Too small!")))
#'user/another-spec-failure-msg
user=> (another-spec-failure-msg)
FAIL in (another-spec-failure-msg) (...:...)
Large number should fail
Too big!
(* 14 30)
val: 420 fails spec: :small/value predicate: (< % 100)
expected: (s/valid? :small/value (* 14 30))
actual: (not (s/valid? :small/value 420))
FAIL in (another-spec-failure-msg) (...:...)
Negative number should fail
Too small!
(* -14 30)
val: -420 fails spec: :small/value predicate: pos-int?
expected: (s/valid? :small/value (* -14 30))
actual: (not (s/valid? :small/value -420))
nil
While the above can already get you further than clojure.test
, Expectations provides a lot more: