Testing Clojure With Expectations

This tutorial guides you though setup of Expectations testing library for Clojure and highlights the most important features.

Brought to you by

Semaphore

Introduction

Clojure is shipped with clojure.test — a library with basic features for writing tests. However, there are a few alternatives that aim to make writing tests more pleasant or more suitable for BDD. Expectations by Jay Fields is one of them, described as "a minimalist's unit testing framework" with the slogan "adding signal, removing noise".

This article explains how you can install Expectations in your Clojure project. We will also show what Expectations brings to the table, that's not already present in clojure.test.

Expectations Setup

The easiest way to get started is to add Expectations and lein-expectations to your project.clj:

(defproject expectations-playground "0.1.0-SNAPSHOT"
  :description "Playground for exploring Expectations"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [expectations "2.0.16"]]
  :plugins [[lein-expectations "0.0.8"]])

lein-expectations is a Leiningen plugin to run tests written using the expectations library. After that, you can run tests with lein expectations.

Expectations also integrates well with several editors and development environments, such as Emacs and IntelliJ. More information about installation and setup can be found in the library documentation.

Adding Signal, Removing Noise

A simple example that compares clojure.test and Expectations already reveals few interesting details:

; clojure.test
(deftest equality-test
  (testing "Is 'foo' equal 'fooer'"
    (is (= "foo" "fooer"))))

; expectations
(expect "foo" "fooer")

Running the test with clojure.test gives:

$ lein test

lein test expectations-playground.core-test

lein test :only expectations-playground.core-test/equality-test

FAIL in (equality-test) (core_test.clj:7)
Is 'foo' equal 'fooer'
expected: (= "foo" "fooer")
  actual: (not (= "foo" "fooer"))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.

While running the test with Expectations gives:

$ lein expectations

failure in (expectations_test.clj:4) : expectations-playground.expectations-test
(expect "foo" "fooer")

           expected: "foo"
                was: "fooer"

           matches: "foo"
           diverges: ""
                  &: "er"

Ran 1 tests containing 1 assertions in 42 msecs
1 failures, 0 errors.

We already see Expectations delivering on the slogan "adding signal, removing noise". The test is shorter, but it gives us more informations in a nicer way. Output is colored by default, format is a bit easier on eyes and it's more precise about the failure — it says what's different about strings. It also shows how much time the test took to run, which can be useful when optimizing tests.

Minimal and Consistent Syntax

It's interesting to see how minimal and consistent the Expectations syntax is. At first glance, there is not much besides expect. It can get you a long way:

(expect 2 (+ 1 1))

(expect "foo" (str "f" "o" "o"))

(expect [1 2 3] (conj [1 2] 3))

(expect {:a 1 :b 2} (assoc {:a 1} :b 2))

(expect #"Expect" "Expectations")

(expect empty? [])

(expect 3 (in [1 2 3]))

With expect you can compare integers, strings, types, regular expressions and collections and test for function output.

Even More Signal

Expectations really shines when it comes to testing collections as it tests not only equality, but also contents of collections:

$ lein expectations

failure in (expectations_test.clj:4) : expectations-playground.expectations-test
(expect [1 2 3] [2 3 1])

           expected: [1 2 3]
                was: [2 3 1]

           in expected, not actual: [1 2 3]
           in actual, not expected: [2 3 1]
           lists appear to contain the same items with different ordering

failure in (expectations_test.clj:6) : expectations-playground.expectations-test
(expect [1 2 3] [1 2 3 4])

           expected: [1 2 3]
                was: [1 2 3 4]

           in expected, not actual: null
           in actual, not expected: [nil nil nil 4]
           actual is larger than expected

failure in (expectations_test.clj:8) : expectations-playground.expectations-test
(expect [1 2 3] [1 2])

           expected: [1 2 3]
                was: [1 2]

           in expected, not actual: [nil nil 3]
           in actual, not expected: null
           expected is larger than actual

Ran 3 tests containing 3 assertions in 73 msecs
3 failures, 0 errors.

Here you can easily see what's different about expected and real value. This is especially important when comparing long or nested collections.

Even Less Noise

Expectations tries aggressively to remove noise from test output. For example, it trims long stack traces, leaving only the important part.

Example stack trace using clojure.test:

ERROR in (bad-aritmetic-test) (Numbers.java:156)
Uncaught exception, not in assertion.
expected: nil
  actual: java.lang.ArithmeticException: Divide by zero
 at clojure.lang.Numbers.divide (Numbers.java:156)
    clojure.lang.Numbers.divide (Numbers.java:3731)
    expectations_playground.core_test/fn (core_test.clj:6)
    clojure.test$test_var$fn__7187.invoke (test.clj:704)
    clojure.test$test_var.invoke (test.clj:704)
    clojure.test$test_vars$fn__7209$fn__7214.invoke (test.clj:722)
    clojure.test$default_fixture.invoke (test.clj:674)
    clojure.test$test_vars$fn__7209.invoke (test.clj:722)
    clojure.test$default_fixture.invoke (test.clj:674)
    clojure.test$test_vars.invoke (test.clj:718)
    clojure.test$test_all_vars.invoke (test.clj:728)
    clojure.test$test_ns.invoke (test.clj:747)
    clojure.core$map$fn__4245.invoke (core.clj:2559)
    clojure.lang.LazySeq.sval (LazySeq.java:40)
    clojure.lang.LazySeq.seq (LazySeq.java:49)
    clojure.lang.Cons.next (Cons.java:39)
    clojure.lang.RT.boundedLength (RT.java:1654)
    clojure.lang.RestFn.applyTo (RestFn.java:130)
    clojure.core$apply.invoke (core.clj:626)
    clojure.test$run_tests.doInvoke (test.clj:762)
    clojure.lang.RestFn.applyTo (RestFn.java:137)
...

This is the equivalent stack trace using Expectations:

failure in (expectations_test.clj:4) : expectations-playground.expectations-test
(expect 1 (/ 1 0))

  act-msg: exception in actual: (/ 1 0)
    threw: class java.lang.ArithmeticException - Divide by zero
           on (expectations_test.clj:4)
           on (form-init1230342255835259211.clj:1)

Ran 1 tests containing 1 assertions in 67 msecs
0 failures, 1 errors.

Expectations output is much shorted, with more useful information.

Conclusion

Expectations certainly delivers on promise "more signal, less noise". But the library also provides a few additional tricks that are not covered here — like testing side effects and freezing time. For more informations, visit the Expectations website.

81dc6254e4ae1e5578bfba393a844bd4
Nebojša Stričević

An independent Ruby on Rails and JavaScript consultant. Visit my website for more info.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.