biff

anselm 2024-10-30T09:57:39.700659Z

hello ๐Ÿ‘‹ new to clojure and biff but has been a fairly smooth start so far ๐Ÿ™‚ however, now I'm a bit stuck writing some basic db tests and haven't found a similar scenario https://github.com/jacobobryant/biff/blob/master/test/com/biffweb/impl/xtdb_test.clj. any help appreciated! details in ๐Ÿงต

anselm 2024-10-31T07:59:25.178209Z

OK thanks for the further context and pointers! Just a learning curve I have to get over ๐Ÿ™‚ While we're at writing tests - do you have some suggestions on writing tests for calling an endpoint? Do you suggest to use something like ring.mock.request or does biff already come with convenience functions? (Thanks for this great package, really makes it quite painless getting started with clojure (web dev))

2024-10-31T15:27:50.879039Z

ring-mock would be fine. In my own tests, I usually call the "base" handler function directly, without going through middleware. i.e. if you have something like this:

(defn foo [request]
  ...)

(def module
  {:routes [["/foo" {:post foo}]]})
then you can write unit tests that just call the foo function directly instead of going through the top-level handler with all the middleware. (And if you wanted you could have separate unit or integration tests that do include the middleware). But it would also be fine to write unit tests for your handlers that do go through the top-level handler function. You just might need to include some extra stuff in the mock requests, e.g. for authentication. Biff doesn't have any test helper functions other than https://github.com/jacobobryant/biff/blob/a1e9b8c37680e5da3d1dae36a450a3eacf00fb0e/src/com/biffweb.clj#L743. I am currently experimenting with some testing approaches though that will probably make it into biff at some point. See https://clojureverse.org/t/separating-effects-from-business-logic/10961 discussing a way to keep handler functions pure. I'm also trying out an approach to example-based tests where you specify only the inputs in code and then you write the outputs to an edn file. The tests ensure that the inputs and outputs haven't changed. If you update a function and the outputs are supposed to change, then instead of manually updating a bunch of test cases, you just call a function that updates the edn file and then look over the git diff. It works nicely with the keeping-handlers-pure stuff because all your unit tests can be simple (is (= <expected> (my-handler ...))) forms; no need to do anything stateful and then check the side effects.

๐Ÿ™ 1
anselm 2024-10-30T10:01:35.758439Z

Schema:

(def schema
  {
   ;; user attributes
   :user/id :uuid
   :user/email :string

   ;; user entity
   :user [:map {:closed true}
          [:xt/id                     :user/id]
          [:user/email                :string]]})
User namespace:
(ns com.example.models.user
  (:require [com.biffweb :as biff]))

(defn create-user! [ctx email]
  (let [uuid (random-uuid)]
  (biff/submit-tx ctx
                  [{:db/doc-type :user
                    :xt/id uuid
                    :db/op :create
                    :user/email email}])
  uuid))

(defn get-user [{:keys [biff/db]} email]
  (biff/lookup db :user/email email))
Tests:
(ns com.example.models.user-test
  (:require
   [clojure.test :refer [deftest is]]
   [com.biffweb :as biff :refer [test-xtdb-node]]
   [com.example :as main]
   [xtdb.api :as xt]
   [com.example.models.user :as user]))

(defn get-context [node]
  {:biff.xtdb/node  node
   :biff/db         (xt/db node)
   :biff/malli-opts #'main/malli-opts})

(def bobs-email "bob@example.com")
(def alices-email "alice@example.com")

(deftest get-user
  (with-open [node (test-xtdb-node [{:xt/id (random-uuid) :user/email bobs-email}])]
    (let [ctx (get-context node)
          existing-user (user/get-user ctx bobs-email)
          non-existing-user (user/get-user ctx alices-email)]
      (is (some? existing-user))
      (is (= (:user/email existing-user) bobs-email))
      (is (nil? non-existing-user)))))

(deftest create-user
  (with-open [node (test-xtdb-node [])]
    (let [ctx (get-context node)]
        (let [created-uuid (user/create-user! ctx bobs-email)
              created-user (user/get-user ctx bobs-email)]
          ;; verify we get the uuid after creation
          (is (some? created-user))
          (is (uuid? created-uuid))))))
Problem: โ€ข the get-user test runs fine โ€ข the create-user-test fails at (is (some? created-user)) Given that the call to create-user goes through without error, I'd assume that the record should have been added to the db. Do I need to somehow refresh it or is there something more fundamentally wrong in any of my code?

anselm 2024-10-30T11:10:26.187149Z

Looks like "refreshing" the db with xt/db did the trick:

(deftest create-user
  (with-open [node (test-xtdb-node [])]
    (let [ctx (get-context node)]
      (user/create-user! ctx bobs-email)
      (let [db (xt/db node) ;; refresh the db
            ctx (assoc ctx :biff/db db) ;; update the context
            created-user (user/get-user ctx bobs-email)] ;; query user
        ;; verify we get the expected user after creating it
        (is (some? created-user))
        (is (uuid? (:xt/id created-user)))
        (is (= bobs-email (:user/email created-user)))))))
I don't understand why this refresh is necessary though..

2024-10-30T19:49:52.933149Z

Yep, calling xt/db again is the solution. you can also do (let [ctx (biff/merge-context ctx) ... as a convenience which updates the :biff/db key in ctx. This is needed because db is an immutable value / snapshot of the database at a particular point in time. You can call (xt/db-basis db) to see the time for which the snapshot was made, and you'll see that the db-basis value changes after getting a new db value with xt/db / biff/merge-context. This is a somewhat common pitfall and it's probably worth addressing somehow... in XT2 I think they've set it up so the default approach is you always get the latest DB value, and then if you want a snapshot you can get that explicitly. or something.