Awesome Open Source
Awesome Open Source

opticlj

opticlj is a Clojure(Script) expectation testing (also known as snapshot testing) library.

Rationale

Expectations, or snapshots, is an automated testing strategy that captures the output of a program as a reference to its correctness. In contrast to unit testing, snapshots don't require the programmer to specify the correct output of their program but instead to verify the output.

opticlj let's you define these snapshots and automatically generate the outputs into files. If you change the implementation of your program, the output files may be checked against the new behavior for differences.

I was inspired by this testing strategy because it navigates elegantly between REPL driven development and testing. Unit testing is often cumbersome, but I've found it to be even more so while writing Clojure code: I often verify the correctness of my functions by simply evaluating them, but that output doesn't persist outside of my own machine.

Snapshot testing may be a way for Clojure developers to cast a wide net over the correctness of their programs while staying close to the REPL.

Use Cases

Snapshot testing is often a great substitute to unit testing, but it in no way has the power to verify programs as thoroughly as property-based testing. Snapshot tests are best used for pure functions, and aren't recommended in cases where correctness must be "proven" (big air quotes).

Inspirations

Installation

[opticlj "1.0.0-alpha10"]

See on Clojars

Disclaimer

opticlj is alpha software, and its API is likely subject to change.

Usage

The below example is a way to get started with opticlj in Clojure.

Require the opticlj.core namespace to get started:

(ns my-project.core-test
  (:require [opticlj.core :as optic :refer [defoptic]]))

Let's define a function to test:

(defn add [x y]
  (+ x y))

Define an optic like so:

(defoptic ::one-plus-one (add 1 1))

This does two things:

  • Defines "runner" function that can be accessed with opticlj.core/run
  • Writes an output file in test/__optic__/my_project/core_test/one_plus_one.clj

Here's what one_plus_one.clj looks like:

(in-ns 'my-project.core-test)

(add 1 1)

2

The in-ns expression allows us to evaluate this file, which is especially useful if your editor integrates with the REPL.

Next, if we change the implementation of add and re-run the optic, we get output confirming the snapshot was checked:

(defn add [x y]
  (+ x y 2))

(run ::one-plus-one)

; outputs
{:file "test/__optic__/my_project/core_test/one_plus_one.clj"
 :err-file "test/__optic__/my_project/core_test/one_plus_one.err.clj"
 :passing? false
 :diff {:string "<truncated>"}
 :form (add 1 1)
 :result 4
 :kw :my-project.core-test/one-plus-one}

A new file was created: test/__optic__/my_project/core_test/one_plus_one.err.clj

(in-ns 'my-project.core-test)

(add 1 1)

4

Also, note how the :passing? key is false. We can view our error diff by calling optic/errors:

(optic/errors)
; prints
--- test/__optic__/my_project/core_test/one_plus_one.clj   2017-09-22 16:03:38.000000000 -0500
+++ -   2017-09-22 16:04:38.000000000 -0500
@@ -2,4 +2,4 @@

 (add 1 1)

-2
+4

What we get back is essentially the output of running:

echo "...my new output..." | diff -u <output-file> -

Let's say we wanted to change the rules of our universe and make the addition of one and one equal to four. We can adjust! our optic to accept these new rules:

(optic/adjust! ::one-plus-one)

; outputs
{:adjusted {:file "test/__optic__/my_project/core_test/one_plus_one.clj"
            :passing? true
            :diff nil
            :err-file nil
            :form (add 1 1)
            :result 4
            :kw :my-project.core-test/one-plus-one}}

Now when we check for errors, we see we have resolved our new form of arithmetic:

(optic/errors)

; outputs
nil

ClojureScript

opticlj supports ClojureScript with a few caveats, namely that in order to run ClojureScript tests, you must output your test code using :target :nodejs in your compiler options. See the test/opticlj/cljs directory for an example of using opticlj with the doo test runner.

A convenience function, opticlj.core/ok?, exists to wrap opticlj's tests in a cljs.test/deftest expression. For example:

(ns my-project.cljs.core-test
  (:require [cljs.test :as test :refer-macros [deftest]]
            [opticlj.core :as optic :refer-macros [defoptic]]))

(defoptic ::two-plus-two (+ 2 2))

(deftest optics
  (test/is (optic/passing? (optic/review!))))

Todo

  • [x] Warn if optics is undefined in the program yet exists in a file
  • [x] Add a clean! method to remove unused optics
  • [x] Use defoptic on defoptic (Inception noise)
  • [ ] Complete API documentation
  • [ ] Reimplement core API with stateless methods

Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Clojure (24,713
Testing (6,217
Repl (683
Snapshot Testing (129
Related Projects