https://github.com/waddie/still
Self-modifying snapshot testing for Clojure/ClojureScript/Babashka
https://github.com/waddie/still
babashka clojure clojurescript snapshot-testing testing works-on-my-machine
Last synced: 14 days ago
JSON representation
Self-modifying snapshot testing for Clojure/ClojureScript/Babashka
- Host: GitHub
- URL: https://github.com/waddie/still
- Owner: waddie
- License: mit
- Created: 2025-11-18T10:13:42.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-05-06T09:48:40.000Z (about 1 month ago)
- Last Synced: 2026-05-06T11:15:00.933Z (about 1 month ago)
- Topics: babashka, clojure, clojurescript, snapshot-testing, testing, works-on-my-machine
- Language: Clojure
- Homepage:
- Size: 1.32 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[](https://clojars.org/dev.tomwaddington/still)
# Still
Self-modifying snapshot testing for Clojure/ClojureScript/Babashka, inspired by [juxt/snap](https://github.com/juxt/snap) and Ian Henry’s ["My Kind of REPL"](https://ianthehenry.com/posts/my-kind-of-repl/) and [Judge](https://github.com/ianthehenry/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.
- Optional colour diffing
- Auto-update mode,
- Custom serialisers to handle timestamps, UUIDs, and custom types
- Snapshot metadata to track creation date, platform, etc.
## Demo

## Installation
### Clojure (deps.edn)
```clojure
{: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.
### Babashka (bb.edn)
```clojure
{:deps {dev.tomwaddington/still {:mvn/version "RELEASE"}
rewrite-clj/rewrite-clj {:mvn/version "1.2.50"}
lambdaisland/deep-diff2 {:mvn/version "2.12.219"}}}
```
## Quick start
```clojure
(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))))
```
## API
### `snap` - Filesystem-based snapshots
Compares a value against a stored snapshot file. Behaviour adapts to three contexts:
**Inside deftest (test context):**
- Uses `clojure.test/is` for assertions
- Failures appear in test runner output
- Integrates with CI/CD pipelines
**Outside deftest in REPL (interactive context):**
- Returns boolean (true if match, false if mismatch)
- Prints friendly messages to `stdout`
- No test framework overhead
**Outside deftest and REPL (assertion context):**
- Throws `AssertionError` on mismatch
- Returns true on match
- No output unless there’s an error
- Prevents noise during namespace loading
```clojure
;; 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.
```clojure
;; 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):
- Use nREPL 1.5.0+ with a supporting client
- OR load the file instead of evaluating individual forms
## Configuration
Configure `still` via multiple sources (later sources override earlier):
1. Default configuration
1. `deps.edn`/`bb.edn`/`project.clj` (`:still/config` key)
1. Environment variables
1. Runtime overrides (highest priority)
### Configuration Options
```clojure
{: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:
```clojure
;; Disable snapshots (compiles out snap! macro completely)
(set! *assert* false)
;; Re-enable
(set! *assert* true)
```
### In deps.edn
```clojure
{:still/config {:snapshot-dir "test/snapshots"
:auto-update? false}}
```
### Runtime Override
```clojure
(require '[still.config :as config])
;; Replace entire config
(config/override! {:snapshot-dir "test/custom"})
;; Merge into config
(config/merge-override! {:auto-update? true})
```
### Environment variables
```sh
export STILL_SNAPSHOT_DIR="test/snapshots"
export STILL_AUTO_UPDATE="false"
```
## Auto-update mode
Update all mismatched snapshots automatically
```sh
# Via environment variable
STILL_AUTO_UPDATE=true clj -M:test
```
```clojure
;; Or programmatically
(require '[still.update :as update])
(update/enable-auto-update!)
```
## Custom serialisers
Handle unstable values like timestamps and UUIDs:
```clojure
(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))
```
### Diff visualisation
Full-colour diffs powered by `deep-diff2`:
```clojure
(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))
```
### Snapshot management
```clojure
(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!)
```
## REPL workflow
`still` is designed for interactive development:
```clojure
;; 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
```
## Running tests
### Clojure
```sh
# 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)"
```
### Babashka
```sh
# Run tests
bb test
# Verify namespaces load
bb verify
# Start REPL with Still loaded
bb repl
# Update snapshots
STILL_AUTO_UPDATE=true bb test
```
## License
Copyright © 2025 Tom Waddington
Distributed under the MIT License. See LICENSE file for details.