@@ -3412,6 +3412,160 @@ with `deftest` and name them `something-test`.
34123412(deftest something ...)
34133413----
34143414
3415+ === Use `testing` Blocks for Context [[use-testing-blocks]]
3416+
3417+ Group related assertions under `testing` to provide context in
3418+ failure output. This makes it clear which scenario failed without
3419+ having to read the assertion itself.
3420+
3421+ [source,clojure]
3422+ ----
3423+ ;; good
3424+ (deftest user-validation-test
3425+ (testing "rejects blank names"
3426+ (is (not (valid? {:name ""}))))
3427+ (testing "accepts valid names"
3428+ (is (valid? {:name "Bruce"}))))
3429+
3430+ ;; bad - no context when an assertion fails
3431+ (deftest user-validation-test
3432+ (is (not (valid? {:name ""})))
3433+ (is (valid? {:name "Bruce"})))
3434+ ----
3435+
3436+ === Prefer Informative Assertion Messages [[informative-assertion-messages]]
3437+
3438+ Add messages to `is` when the assertion alone doesn't explain the
3439+ failure.
3440+
3441+ [source,clojure]
3442+ ----
3443+ ;; good - the failure message explains what went wrong
3444+ (is (= 200 (:status response)) "Expected HTTP 200 for valid input")
3445+
3446+ ;; ok - obvious assertions don't need a message
3447+ (is (true? (even? 4)))
3448+ ----
3449+
3450+ === Use `are` for Tabular Tests [[use-are-for-tabular-tests]]
3451+
3452+ When testing the same logic with many inputs, prefer `are` over
3453+ repetitive `is` forms.
3454+
3455+ [source,clojure]
3456+ ----
3457+ ;; good
3458+ (deftest palindrome?-test
3459+ (are [s expected] (= expected (palindrome? s))
3460+ "racecar" true
3461+ "hello" false
3462+ "madam" true
3463+ "" true))
3464+
3465+ ;; bad - repetitive
3466+ (deftest palindrome?-test
3467+ (is (true? (palindrome? "racecar")))
3468+ (is (false? (palindrome? "hello")))
3469+ (is (true? (palindrome? "madam")))
3470+ (is (true? (palindrome? ""))))
3471+ ----
3472+
3473+ === Test One Concept per `deftest` [[one-concept-per-deftest]]
3474+
3475+ Each `deftest` should cover one logical behavior. Use `testing`
3476+ blocks for sub-cases rather than separate `deftest` forms for every
3477+ edge case.
3478+
3479+ === Use `with-redefs` Sparingly [[use-with-redefs-sparingly]]
3480+
3481+ Use `with-redefs` only for external boundaries (HTTP, database,
3482+ clock). Prefer designing functions to take dependencies as arguments
3483+ instead.
3484+
3485+ [source,clojure]
3486+ ----
3487+ ;; good - inject the dependency
3488+ (defn fetch-users [http-get]
3489+ (http-get "/api/users"))
3490+
3491+ (deftest fetch-users-test
3492+ (is (= [{:name "Bruce"}]
3493+ (fetch-users (constantly [{:name "Bruce"}])))))
3494+
3495+ ;; ok for integration-level tests
3496+ (deftest fetch-users-integration-test
3497+ (with-redefs [http/get (constantly {:body [{:name "Bruce"}]})]
3498+ (is (= [{:name "Bruce"}] (fetch-users)))))
3499+ ----
3500+
3501+ === Test Expected Exceptions [[test-expected-exceptions]]
3502+
3503+ Use `thrown?` and `thrown-with-msg?` to assert that code raises the
3504+ expected exception.
3505+
3506+ [source,clojure]
3507+ ----
3508+ ;; good
3509+ (deftest division-test
3510+ (is (thrown? ArithmeticException (/ 1 0)))
3511+ (is (thrown-with-msg? ExceptionInfo #"Invalid" (validate! nil))))
3512+ ----
3513+
3514+ === Prefer `match?` for Structural Assertions [[prefer-match-for-structural-assertions]]
3515+
3516+ Consider using `match?` from
3517+ https://github.com/nubank/matcher-combinators[matcher-combinators] when
3518+ testing functions that return maps with generated or dynamic fields
3519+ (IDs, timestamps). It lets you assert on structure and types without
3520+ over-specifying, and provides clear diffs on failure.
3521+
3522+ [source,clojure]
3523+ ----
3524+ (require '[matcher-combinators.test])
3525+ (require '[matcher-combinators.matchers :as m])
3526+
3527+ ;; good - asserts structure and types, resilient to new fields
3528+ (is (match? {:id uuid?
3529+ :name "Alice"
3530+ :status :active
3531+ :created-at inst?}
3532+ (create-user! {:name "Alice"})))
3533+
3534+ ;; bad - brittle, breaks when a new field is added
3535+ (is (= {:id "abc-123"
3536+ :name "Alice"
3537+ :status :active
3538+ :created-at #inst "2024-01-01"}
3539+ (create-user! {:name "Alice"})))
3540+
3541+ ;; also bad - loses structural context
3542+ (let [result (create-user! {:name "Alice"})]
3543+ (is (= "Alice" (:name result)))
3544+ (is (= :active (:status result)))
3545+ (is (uuid? (:id result))))
3546+ ----
3547+
3548+ === Use `thrown-match?` for Exception Data [[use-thrown-match-for-exception-data]]
3549+
3550+ When testing `ex-info` exceptions, prefer `thrown-match?` from
3551+ matcher-combinators over `try`/`catch` boilerplate. It asserts on the
3552+ exception data map structurally, just like `match?`.
3553+
3554+ [source,clojure]
3555+ ----
3556+ ;; good
3557+ (is (thrown-match? ExceptionInfo
3558+ {:type :validation-error :field :email}
3559+ (validate! {:email "bad"})))
3560+
3561+ ;; bad - verbose and easy to get wrong
3562+ (is (thrown? ExceptionInfo
3563+ (try (validate! {:email "bad"})
3564+ (catch ExceptionInfo e
3565+ (is (= :validation-error (:type (ex-data e))))
3566+ (throw e)))))
3567+ ----
3568+
34153569== Library Organization
34163570
34173571=== Library Coordinates [[lib-coordinates]]
0 commit comments