Testing
See Testing Strategy for the conceptual overview. This guide covers the day-to-day commands and patterns.
Test commands
# Every test surface (monorepo: also the standalone libs and tools)
bb test:all
# Every tests.edn suite
clojure -M:test:test/all
# Single library
clojure -M:test :core
clojure -M:test :user
# By tag
clojure -M:test:test/all --focus-meta :unit
clojure -M:test:test/all --focus-meta :integration
clojure -M:test:test/all --focus-meta :contract
# Single namespace
clojure -M:test:test/all --focus user-property-test
# Watch mode
clojure -M:test --watch :core
clojure -M:test:test/all --watch --focus-meta :unit
# Auth tests (JWT secret required)
JWT_SECRET="dev-secret-at-least-32-characters-long" clojure -M:test :user
Writing unit tests
Tag with ^:unit. Call core functions directly - no setup needed:
(ns wagoe.product.core.product-test
(:require [clojure.test :refer [deftest is testing]]
[wagoe.product.core.product :as product-core]))
(deftest ^:unit test-prepare-product
(testing "returns a product with a generated id"
(let [now (java.time.Instant/now)
result (product-core/prepare-product {:name "Widget" :price 9.99M} now)]
(is (uuid? (:id result)))
(is (= "Widget" (:name result)))
(is (= 9.99M (:price result)))))
(testing "adds timestamps"
(let [now (java.time.Instant/now)
result (product-core/prepare-product {:name "Widget" :price 9.99M} now)]
(is (inst? (:created-at result)))
(is (inst? (:updated-at result))))))
Writing integration tests
Tag with ^:integration. Use in-memory adapters to test service orchestration:
(ns wagoe.product.shell.service-test
(:require [clojure.test :refer [deftest is use-fixtures]]
[wagoe.product.shell.service :as service]
[wagoe.product.shell.adapters.in-memory :as mem]))
(defn with-service [f]
(let [repo (mem/create-in-memory-repo)]
(binding [*service* (service/create-product-service {:repo repo})]
(f))))
(use-fixtures :each with-service)
(deftest ^:integration test-create-product
(let [result (service/create-product *service* {:name "Widget" :price 9.99M})]
(is (= "Widget" (:name result)))
(is (uuid? (:id result)))))
Writing contract tests
Tag with ^:contract. Test against a real H2 in-memory database; wagoe.test
(the test library) boots the app’s system on the test profile and drops the
database afterwards:
(ns wagoe.product.shell.persistence-test
(:require [clojure.test :refer [deftest is]]
[my-app.system-config :as system-config]
[wagoe.product.shell.persistence :as persistence]
[wagoe.test :refer [with-test-system]]))
(deftest ^:contract test-round-trip
(with-test-system [sys system-config/ig-config]
(let [db (:wagoe/db-context sys)
product {:id (random-uuid) :name "Widget" :price 9.99M
:created-at (java.time.Instant/now)}
_ (persistence/create-product! db product)
found (persistence/find-product-by-id db (:id product))]
(is (= (:name product) (:name found)))
(is (= (:price product) (:price found))))))
Test suite in tests.edn
Add new libraries to tests.edn:
#kaocha/v1
{:kaocha/source-paths [;; existing paths ...
"libs/product/src"]
:tests [;; existing suites ...
{:id :product
:test-paths ["libs/product/test"]
:ns-patterns ["wagoe.product.*-test"]}]}
AI test generation
bb ai gen-tests src/<your-project>/product/core/product.clj
Generates a complete test namespace from the source file using Wagoe conventions.
The model sees the real protocols and vars of every namespace the source requires.
It prints to stdout; --write writes it to the conventional test path, -o to a file you name.
The answer is linted as bb check lints it, and retried once with the
errors; if it still fails nothing is written unless you pass --force. It is
run, in a separate JVM, only with --run. Each
deftest is tagged ^:unit unless it does real I/O.