Skip to content

Commit 6d83c1c

Browse files
committed
Expand the Testing section with practical guidelines
Add six new rules: use testing blocks for context, informative assertion messages, are for tabular tests, one concept per deftest, use with-redefs sparingly, and test expected exceptions. Brings the section from 3 to 9 rules.
1 parent 07bc1c9 commit 6d83c1c

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

README.adoc

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)