Files
integreat/.claude/skills/testing-conventions/SKILL.md
2026-01-31 10:13:06 -08:00

7.9 KiB

name, description
name description
testing-conventions Describe the way that tests should be authored, conventions, tools, helpers, superceding any conventions found in existing tests.

Testing Conventions Skill

This skill documents the testing conventions for test/clj/auto_ap/.

Test Focus: User-Observable Behavior

Primary rule: Test user-observable behavior. If an endpoint or function makes a database change, verify the change by querying the database directly rather than asserting on markup.

other rules:

  1. Don't test the means of doing work. For example, if there is a middleware that makes something available on a request, don't bother testing that wrapper.
  2. prefer :refer testing imports, rather than :as reference
  3. Prefer structured edits from clojure-mcp

When to Assert on Database State

When testing an endpoint that modifies data:

  1. Verify the database change by querying the entity directly
  2. Use dc/pull or dc/q to verify the data was stored correctly
;; CORRECT: Verify the database change directly
(deftest test-create-transaction
  (let [result @(post-create-transaction {:amount 100.0})]
    (let [entity (dc/pull (dc/db conn) [:db/id :transaction/amount] (:transaction/id result))]
      (is (= 100.0 (:transaction/amount entity))))))

;; CORRECT: Verify response status and headers
(is (= 201 (:status response)))
(is (= "application/json" (get-in response [:headers "content-type"])))

;; CORRECT: Check for expected text content
(is (re-find #"Transaction created" (get-in response [:body "message"])))

When Markup Testing is Acceptable

Markup testing (HTML/SSR response bodies) is acceptable when:

  • Validating response status codes and headers
  • Checking for presence/absence of specific text strings
  • Verifying small, expected elements within the markup
  • Testing SSR component rendering
;; ACCEPTABLE: Response codes and headers
(is (= 200 (:status response)))
(is (= "application/json" (get-in response [:headers "content-type"])))

;; ACCEPTABLE: Text content within markup
(is (re-find #"Transaction found" response-body))

;; ACCEPTABLE: Small element checks
(is (re-find #">Amount: \$100\.00<" response-body))

When to Avoid Markup Testing

Do not use markup assertions for:

  • Verifying complex data structures (use database queries instead)
  • Complex nested content that's easier to query
  • Business logic verification (test behavior, not presentation)

Database Setup

All tests in test/clj/auto_ap/ use a shared database fixture (wrap-setup) that:

  1. Creates a temporary in-memory Datomic database (datomic:mem://test)
  2. Loads the full schema from io/resources/schema.edn
  3. Installs custom Datomic functions from io/resources/functions.edn
  4. Cleans up the database after each test

Using the Fixture

(ns my-test
  (:require
   [auto-ap.integration.util :refer [wrap-setup]]
   [clojure.test :as t]))

(use-fixtures :each wrap-setup)

(deftest my-test
  ;; tests here can access the test database
  )

Helper Functions

test/clj/auto_ap/integration/util.clj provides helper functions for creating test data:

Identity Helpers

;; Add a unique string to avoid collisions
(str "CLIENT" (rand-int 100000))
(str "INVOICE " (rand-int 1000000))

Test Entity Builders

;; Client
(test-client
  [:db/id "client-id"
   :client/code "CLIENT123"
   :client/locations ["DT" "MH"]
   :client/bank-accounts [:bank-account-id]])

;; Vendor
(test-vendor
  [:db/id "vendor-id"
   :vendor/name "Vendorson"
   :vendor/default-account "test-account-id"])

;; Bank Account
(test-bank-account
  [:db/id "bank-account-id"
   :bank-account/code "TEST-BANK-123"
   :bank-account/type :bank-account-type/check])

;; Transaction
(test-transaction
  [:db/id "transaction-id"
   :transaction/date #inst "2022-01-01"
   :transaction/client "test-client-id"
   :transaction/bank-account "test-bank-account-id"
   :transaction/id (str (java.util.UUID/randomUUID))
   :transaction/amount 100.0
   :transaction/description-original "original description"])

;; Payment
(test-payment
  [:db/id "test-payment-id"
   :payment/date #inst "2022-01-01"
   :payment/client "test-client-id"
   :payment/bank-account "test-bank-account-id"
   :payment/type :payment-type/check
   :payment/vendor "test-vendor-id"
   :payment/amount 100.0])

;; Invoice
(test-invoice
  [:db/id "test-invoice-id"
   :invoice/date #inst "2022-01-01"
   :invoice/client "test-client-id"
   :invoice/status :invoice-status/unpaid
   :invoice/import-status :import-status/imported
   :invoice/total 100.0
   :invoice/outstanding-balance 100.00
   :invoice/vendor "test-vendor-id"
   :invoice/invoice-number "INVOICE 123456"
   :invoice/expense-accounts
   [{:invoice-expense-account/account "test-account-id"
     :invoice-expense-account/amount 100.0
     :invoice-expense-account/location "DT"}]])

;; Account
(test-account
  [:db/id "account-id"
   :account/name "Account"
   :account/type :account-type/asset])

Common Data Setup (setup-test-data)

Creates a minimal but complete dataset for testing:

(defn setup-test-data [data]
  (:tempids @(dc/transact conn (into data
                                    [(test-account :db/id "test-account-id")
                                     (test-client :db/id "test-client-id"
                                                  :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")])
                                     (test-vendor :db/id "test-vendor-id")
                                     {:db/id "accounts-payable-id"
                                      :account/name "Accounts Payable"
                                      :db/ident :account/accounts-payable
                                      :account/numeric-code 21000
                                      :account/account-set "default"}]))))

Use like:

(let [{:strs [test-client-id test-bank-account-id test-vendor-id]} (setup-test-data [])]
  ...)

Token Helpers

;; Admin token
(admin-token)

;; User token (optionally scoped to specific client)
(user-token)           ; Default: client-id 1
(user-token client-id) ; Scoped to specific client

Example Usage

(ns my-test
  (:require
   [clojure.test :as t]
   [auto-ap.datomic :refer [conn]]
   [auto-ap.integration.util :refer [wrap-setup admin-token setup-test-data test-transaction]]))

(use-fixtures :each wrap-setup)

(deftest test-transaction-import
  (testing "Should import a transaction"
    (let [{:strs [client-id bank-account-id]} (setup-test-data [])
          tx-result @(dc/transact conn
                                  [(test-transaction
                                    {:db/id "test-tx-id"
                                     :transaction/client client-id
                                     :transaction/bank-account bank-account-id
                                     :transaction/amount 50.0})])]
      (is (= 1 (count (:tx-data tx-result))))
      ;; Verify by querying the database, not markup
      (let [entity (dc/pull (dc/db conn) [:transaction/amount] (:db/id tx-result))]
        (is (= 50.0 (:transaction/amount entity)))))))

Note on Temp IDs

Test data often uses string-based temp IDs like "client-id", "bank-account-id", etc. When transacting, the returned :tempids map maps these symbolic IDs to Datomic's internal entity IDs:

(let [{:strs [client-id bank-account-id]} (:tempids @(dc/transact conn txes))]
  ...)

Memory Database

All tests use datomic:mem://test - an in-memory database. This ensures:

  • Tests are fast
  • Tests don't interfere with each other
  • No setup required to run tests locally

The database is automatically deleted after each test completes.

running tests

use leiningen to run tests