Add 8 BDD-style tests for the vendors module covering grid/list operations and vendor merge functionality. Tests follow established patterns from accounts_test.clj and include proper database verification. Tests Implemented: - vendor-grid-loads-with-empty-database - vendor-fetch-ids-returns-correct-structure - vendor-fetch-page-returns-vendors - vendor-hydrate-results-works - vendor-merge-transfers-references - vendor-merge-same-vendor-rejected - vendor-merge-invalid-vendor-handled - vendor-hydration-includes-all-fields Key Implementation Details: - Uses setup-test-data helper with unique temp IDs - Tests focus on public interface (fetch-page, merge-submit) - Follows BDD Given/When/Then pattern - All 8 tests passing (26 assertions) Documentation: - Created implementation plan in docs/plans/ - Documented solution patterns in docs/solutions/ - Created code review todos for future improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
235 lines
9.1 KiB
Markdown
235 lines
9.1 KiB
Markdown
---
|
|
module: auto-ap.ssr.admin
|
|
date: 2026-02-06
|
|
problem_type: test_failure
|
|
component: testing_framework
|
|
symptoms:
|
|
- Test assertions failed when extracting entity IDs from Datomic query results
|
|
- Entity reference queries returned entity IDs instead of actual values
|
|
- Numeric code comparisons failed (expected number, got string)
|
|
- Server responses didn't include Datomic tempids for created entities
|
|
root_cause: test_isolation
|
|
resolution_type: test_fix
|
|
severity: medium
|
|
tags: [atomic-query, datomic, entity-references, test-patterns, database-queries]
|
|
---
|
|
|
|
# Datomic Query Patterns in BDD Tests
|
|
|
|
## Problem
|
|
|
|
When writing BDD-style tests for SSR admin operations, test assertions frequently failed due to improper handling of Datomic query results and entity references. The Datomic API behaves differently than standard Clojure collections, causing tests to fail even when the underlying application logic was correct.
|
|
|
|
## Environment
|
|
|
|
- Module: auto-ap.ssr.admin
|
|
- Date: 2026-02-06
|
|
- Affected Component: auto-ap.ssr.admin.accounts-test
|
|
- Test Framework: clojure.test
|
|
- Database: Datomic
|
|
|
|
## Symptoms
|
|
|
|
- **Assertion failures on entity ID extraction**: `(account-id (first accounts))` returned `[entity-id]` (a list) instead of just the entity-id
|
|
- **Entity reference resolution failures**: Pull queries returned `{:account/type :db/id-12345}` (entity reference) instead of `{:account/type :account-type/asset}` (actual value)
|
|
- **Type mismatch errors**: Tests failed when comparing expected numeric code "12345" (string) to actual numeric code 12345 (number)
|
|
- **Tempid unavailability**: Server HTTP responses didn't include Datomic tempids for created entities
|
|
|
|
## What Didn't Work
|
|
|
|
**Attempted Solution 1: Using `first` on query results**
|
|
```clojure
|
|
(let [accounts (dc/q '[:find ?e :where [?e :account/name "TestAccount"]] db)]
|
|
(is (= expected-id (account-id (first accounts)))))
|
|
```
|
|
- **Why it failed**: Datomic queries return tuples, and `(first accounts)` returns a tuple `[entity-id]` (a list form), not just the entity-id
|
|
|
|
**Attempted Solution 2: Direct entity reference in pull**
|
|
```clojure
|
|
'[:account/type]
|
|
```
|
|
- **Why it failed**: Pull queries return entity references (like `:db/id-12345`) for schema attributes, not their actual values
|
|
|
|
**Attempted Solution 3: String comparison for numeric codes**
|
|
```clojure
|
|
(is (= "12345" (:account/numeric-code account)))
|
|
```
|
|
- **Why it failed**: Account numeric codes are stored as numbers in Datomic, not strings. The comparison failed due to type mismatch
|
|
|
|
**Attempted Solution 4: Checking tempids in server response**
|
|
```clojure
|
|
(is (some #(= expected-id %) (get-in result [:data :tempids])))
|
|
```
|
|
- **Why it failed**: SSR controllers return HTTP responses with standard fields (status, body, headers), not Datomic internal tempids
|
|
|
|
## Solution
|
|
|
|
### LEARNING #1: Use `ffirst` to Extract Entity IDs from Datomic Tuples
|
|
|
|
```clojure
|
|
; ❌ WRONG - Returns [entity-id] (a list form)
|
|
(account-id (first accounts))
|
|
|
|
; ✅ CORRECT - Extracts entity-id from the tuple
|
|
(account-id (ffirst accounts))
|
|
```
|
|
|
|
**Explanation**: Datomic queries return collections of tuples. Each tuple contains the result values in order. `(first accounts)` returns the first tuple as a list form `[entity-id]`, which cannot be destructured directly. `ffirst` applies `first` twice: first to get the tuple list, second to get the first element of the tuple (the entity-id).
|
|
|
|
**Best practice**: Always use `ffirst` or apply proper destructuring when working with Datomic query results.
|
|
|
|
### LEARNING #2: Include `[:db/ident]` to Resolve Entity References
|
|
|
|
```clojure
|
|
; ❌ WRONG - Returns entity reference
|
|
'[:account/type]
|
|
|
|
; ✅ CORRECT - Returns actual enum value
|
|
'[:account/type [:db/ident]]
|
|
```
|
|
|
|
**Access pattern**:
|
|
```clojure
|
|
; Extract the actual enum value from the entity
|
|
(:db/ident (:account/type account)) ; Returns :account-type/asset
|
|
```
|
|
|
|
**Explanation**: When querying entity attributes that reference other entities (like `account/type` referencing `account-type/asset`), Datomic returns the entity ID as a reference. Including `[:db/ident]` in the pull expression tells Datomic to fetch the actual value identifier, not the entity reference.
|
|
|
|
**Use case**: Essential when asserting on enum values or type-safe attributes in tests.
|
|
|
|
### LEARNING #3: Use Numbers for Numeric Codes, Not Strings
|
|
|
|
```clojure
|
|
; ❌ WRONG - Numeric code stored as number, not string
|
|
(is (= "12345" (:account/numeric-code account)))
|
|
|
|
; ✅ CORRECT - Numeric code is stored as a number
|
|
(is (= 12345 (:account/numeric-code account)))
|
|
```
|
|
|
|
**Explanation**: Datomic stores numeric attributes as numbers (`double`), even though they're defined as numeric code strings in the application domain. The database stores them as numbers; the API returns them as numbers.
|
|
|
|
**Best practice**: Always use numeric types when asserting on numeric codes, not string equivalents.
|
|
|
|
### LEARNING #4: Query the Database Directly for Verification
|
|
|
|
```clojure
|
|
; ❌ WRONG - Expected tempids in server response
|
|
(let [result (sut/account-save {...})
|
|
response (:response result)]
|
|
(is (contains? (:data response) :tempids)))
|
|
|
|
; ✅ CORRECT - Query database to verify entity was created
|
|
(let [result (sut/account-save {...})
|
|
db (dc/db conn)
|
|
accounts (dc/q '[:find ?e :where [?e :account/name "TestAccount"]] db)]
|
|
(is (seq accounts))) ; Query directly to verify
|
|
```
|
|
|
|
**Explanation**: SSR controllers return HTTP responses without Datomic-specific details. Tempids are internal Datomic identifiers not exposed in HTTP responses. To verify database operations, always query the database directly after the operation.
|
|
|
|
**Best practice**: For database-backed operations in tests, query the database after the operation to verify results.
|
|
|
|
## Why This Works
|
|
|
|
1. **What was the ROOT CAUSE of the problem?**
|
|
- Datomic queries return collections of tuples, not simple collections
|
|
- Entity references in Datomic need explicit resolution through `:db/ident`
|
|
- Numeric attributes in Datomic are stored as numbers, not strings
|
|
- SSR controllers don't expose Datomic internal state (tempids, internal IDs)
|
|
|
|
2. **Why does the solution address this root cause?**
|
|
- `ffirst` properly extracts entity IDs from Datomic tuples
|
|
- Including `[:db/ident]` in pull expressions resolves entity references to their actual values
|
|
- Using numeric types matches Datomic's storage format
|
|
- Querying the database directly accesses the truth source without relying on partial response data
|
|
|
|
3. **What was the underlying issue?**
|
|
- The Datomic API has specific behaviors that differ from standard Clojure collections
|
|
- Entity references are lazy and need explicit resolution
|
|
- Database storage types must be matched in test assertions
|
|
- SSR architecture doesn't expose internal database details in HTTP responses
|
|
- Tests must query the database directly to verify persisted data
|
|
|
|
## Prevention
|
|
|
|
### Test Writing Best Practices
|
|
|
|
1. **Always use `ffirst` for Datomic query results**
|
|
```clojure
|
|
; Standard pattern
|
|
(let [results (dc/q query-string db)
|
|
entity-id (ffirst results)] ; Not: (first results)
|
|
...)
|
|
```
|
|
|
|
2. **Include `[:db/ident]` for entity attribute resolution**
|
|
```clojure
|
|
; Standard pattern for enum values
|
|
'[:attribute [:db/ident]]
|
|
```
|
|
|
|
3. **Use correct data types in assertions**
|
|
```clojure
|
|
; Check attribute types match database
|
|
(is (instance? Long (:numeric-code account))) ; Not: String
|
|
```
|
|
|
|
4. **Query database for verification**
|
|
```clojure
|
|
; Standard pattern: operation → verify with database query
|
|
(let [result (sut/create-resource {...})
|
|
db (dc/db conn)
|
|
entity (dc/q '[:find ?e :where [?e :id ?id]] db)]
|
|
(is (seq entity))) ; Query directly
|
|
```
|
|
|
|
5. **Review Datomic-specific behaviors before writing assertions**
|
|
- Understand that queries return tuples
|
|
- Know that entity references need resolution
|
|
- Remember numeric type storage
|
|
- Accept that SSR responses don't include internal IDs
|
|
|
|
### Code Review Checklist
|
|
|
|
- [ ] Entity IDs extracted with `ffirst` from Datomic queries
|
|
- [ ] Entity references resolved with `[:db/ident]`
|
|
- [ ] Numeric attributes compared as numbers, not strings
|
|
- [ ] Database queries used for verification, not partial responses
|
|
- [ ] Datomic-specific behaviors documented in comments
|
|
|
|
### Test Utility Helpers (Recommended)
|
|
|
|
Consider creating helper functions in your test library to encapsulate these patterns:
|
|
|
|
```clojure
|
|
(ns auto-ap.ssr.test-helpers
|
|
(:require [datomic.api :as dc]))
|
|
|
|
(defn get-entity-by-attribute [conn attribute value]
|
|
"Retrieve entity by attribute-value pair from Datomic database.
|
|
Returns entity or nil if not found."
|
|
(ffirst
|
|
(dc/q '[:find ?e
|
|
:where [?e ?attr val]
|
|
[val ?attribute ?value]]
|
|
(dc/db conn)
|
|
attribute value)))
|
|
|
|
(defn resolve-attribute [entity attribute]
|
|
"Resolve an entity reference attribute to its value.
|
|
If attribute is a reference, returns :db/ident; otherwise returns value."
|
|
(if (map? (attribute entity))
|
|
(get-in entity [attribute :db/ident])
|
|
(attribute entity)))
|
|
```
|
|
|
|
## Related Issues
|
|
|
|
No related issues documented yet.
|
|
|
|
---
|
|
|
|
**Keywords**: atomic-query, datomic, entity-references, test-patterns, database-queries, ffirst, pull-queries, entity-resolution
|