Self-modifying snapshot testing for Clojure/ClojureScript/Babashka, inspired by juxt/snap and Ian Henry’s "My Kind of REPL" and Judge.
still.core/snap saves snapshots to the filesystem. still.core/snap! will modify your source code in place.
Snapshots behave like clojure.test/is when inside a deftest, like standard assertions when used inline, with REPL-friendly output in interactive sessions.

{:deps {dev.tomwaddington/still {:mvn/version "RELEASE"}}
:aliases {:repl {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}}}}}
Note: For best REPL experience with snap!, use nREPL 1.5.0 or later with a client supporting filenames in regular eval. This enables automatic file detection during REPL eval operations.
{:deps {dev.tomwaddington/still {:mvn/version "RELEASE"}
rewrite-clj/rewrite-clj {:mvn/version "1.2.50"}
lambdaisland/deep-diff2 {:mvn/version "2.12.219"}}}
(ns my-app.test
(:require [clojure.test :refer [deftest testing is]]
[still.core :refer [snap snap!]]))
(deftest user-creation-test
(testing "creates user with correct shape"
(let [user (create-user {:name "Alice" :email "alice@example.com"})]
;; First run: creates snapshot in test/still/user_creation.edn
;; Subsequent runs: compares against stored snapshot
(snap :user-creation user))))
(deftest inline-snapshot-test
(testing "inline snapshots"
;; First run: edits this file to add expected value
;; Becomes: (snap! (compute-result) {:result 42})
(snap! (compute-result))))
snap - Filesystem-based snapshotsCompares a value against a stored snapshot file. Behaviour adapts to three contexts:
Inside deftest (test context):
clojure.test/is for assertionsOutside deftest in REPL (interactive context):
stdoutOutside deftest and REPL (assertion context):
AssertionError on mismatch;; In a test
(deftest api-test
(snap :api-response (fetch-data)))
;; In the REPL
(snap :api-response (fetch-data))
;; => ✓ Snapshot matches: :api-response
;; => true
;; Disable all snapshots using *assert*
(set! *assert* false)
(snap :any-key {:any "value"})
;; => true (always passes, no checking)
snap! - inline snapshots (JVM/Babashka only)Like snap, but stores expected values directly in source code. When called without an expected value, automatically edits the source file.
;; First run - edits source file
(snap! (+ 1 2))
;; After first run, the line becomes:
(snap! (+ 1 2) 3)
;; Subsequent runs compare against inline value
REPL usage: For snap! to work when evaluating forms in the REPL (not loading files):
Configure still via multiple sources (later sources override earlier):
deps.edn/bb.edn/project.clj (:still/config key){:snapshot-dir "test/still" ; Where snapshots are stored
:auto-update? false ; Auto-update mismatched snapshots
:metadata? true ; Track snapshot metadata
:serializers {} ; Custom type serialisers
:diff-context-lines 3 ; Context lines in diffs
:color? false} ; ANSI colours in output
; Note: :colour? and :serialisers are also accepted
Enable/Disable: Use Clojure’s *assert* to enable/disable snapshots:
;; Disable snapshots (compiles out snap! macro completely)
(set! *assert* false)
;; Re-enable
(set! *assert* true)
{:still/config {:snapshot-dir "test/snapshots"
:auto-update? false}}
(require '[still.config :as config])
;; Replace entire config
(config/override! {:snapshot-dir "test/custom"})
;; Merge into config
(config/merge-override! {:auto-update? true})
export STILL_SNAPSHOT_DIR="test/snapshots"
export STILL_AUTO_UPDATE="false"
Update all mismatched snapshots automatically
# Via environment variable
STILL_AUTO_UPDATE=true clj -M:test
;; Or programmatically
(require '[still.update :as update])
(update/enable-auto-update!)
Handle unstable values like timestamps and UUIDs:
(require '[still.serialize :as serialize])
;; Timestamps are automatically serialised as ISO-8601
(snap :with-timestamp {:id 123 :created-at (java.util.Date.)})
;; => {:id 123 :created-at {:type :still.serialize/date :iso8601 "2025-..."}}
;; Register custom serialiser for your types
(defrecord Person [name age])
(serialize/register-serializer! Person
(fn [p] {:type ::person :name (:name p) :age (:age p)}))
(snap :person (->Person "Alice" 30))
Full-colour diffs powered by deep-diff2:
(require '[still.diff :as diff])
;; Generate and print a diff
(diff/print-diff {:a 1 :b 2} {:a 1 :b 3})
;; Get diff as string
(diff/diff-str expected actual)
;; Side-by-side comparison
(println (diff/side-by-side expected actual))
(require '[still.update :as update])
;; List all snapshots
(update/print-summary)
;; Enable auto-update for session
(update/enable-auto-update!)
;; Delete all snapshots (careful!)
(update/delete-all-snapshots!)
still is designed for interactive development:
;; Load your namespace
(require '[my-app.core :as core])
(require '[still.core :refer [snap snap!]])
;; Test a function interactively
(snap :user-response (core/create-user {:name "Alice"}))
;; => ✓ Snapshot created: :user-response
;; => true
;; Modify the function, run again
(snap :user-response (core/create-user {:name "Alice"}))
;; => ✗ Snapshot mismatch: :user-response
;; => (shows colourful diff)
;; => false
;; Looks good? Update the snapshot
(config/merge-override! {:auto-update? true})
(snap :user-response (core/create-user {:name "Alice"}))
;; => ✓ Snapshot updated: :user-response
;; => true
# Run tests
clj -M:test -m cognitect.test-runner
# Update all snapshots
STILL_AUTO_UPDATE=true clj -M:test -m cognitect.test-runner
# Disable assertions (and snapshots) at compile time
clj -M:compile -e "(set! *assert* false)"
# Run tests
bb test
# Verify namespaces load
bb verify
# Start REPL with Still loaded
bb repl
# Update snapshots
STILL_AUTO_UPDATE=true bb test
Copyright © 2025 Tom Waddington
Distributed under the MIT License. See LICENSE file for details.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |