juxt.pro - Testable Clojurescript apps









Search Preview

JUXT: Blog: Testable Clojurescript apps

juxt.pro
Using re-frame, devcards and convention to build web apps on solid ground
.pro > juxt.pro

SEO audit: Content analysis

Language Error! No language localisation is found.
Title JUXT: Blog: Testable Clojurescript apps
Text / HTML ratio 62 %
Frame Excellent! The website does not use iFrame solutions.
Flash Excellent! The website does not have any flash contents.
Keywords cloud data suggestions typeahead model query user code server view fn spec reframe db loading? _ values state render testing make
Keywords consistency
Keyword Content Title Description Headings
data 33
suggestions 33
typeahead 22
model 20
query 19
user 17
Headings
H1 H2 H3 H4 H5 H6
1 8 6 0 0 0
Images We found 20 images on this web page.

SEO Keywords (Single)

Keyword Occurrence Density
data 33 1.65 %
suggestions 33 1.65 %
typeahead 22 1.10 %
model 20 1.00 %
query 19 0.95 %
user 17 0.85 %
code 15 0.75 %
server 14 0.70 %
view 13 0.65 %
fn 12 0.60 %
spec 11 0.55 %
reframe 11 0.55 %
db 11 0.55 %
loading? 11 0.55 %
_ 9 0.45 %
values 8 0.40 %
state 8 0.40 %
render 7 0.35 %
testing 7 0.35 %
make 7 0.35 %

SEO Keywords (Two Word)

Keyword Occurrence Density
the user 12 0.60 %
the server 11 0.55 %
of the 9 0.45 %
to the 9 0.45 %
the data 9 0.45 %
suggestions loading? 7 0.35 %
typeahead query 7 0.35 %
we can 7 0.35 %
the view 7 0.35 %
so that 6 0.30 %
the spec 6 0.30 %
I can 6 0.30 %
user types 6 0.30 %
our model 6 0.30 %
to make 5 0.25 %
is a 5 0.25 %
need to 5 0.25 %
db _ 5 0.25 %
types something 4 0.20 %
model can 4 0.20 %

SEO Keywords (Three Word)

Keyword Occurrence Density Possible Spam
user types something 4 0.20 % No
so that it 4 0.20 % No
our model can 4 0.20 % No
fn db _ 4 0.20 % No
to the user 4 0.20 % No
suggestions loading? false 4 0.20 % No
state of the 4 0.20 % No
the user types 3 0.15 % No
what the user 3 0.15 % No
suggestions typeahead query 3 0.15 % No
suggestions loading? true 3 0.15 % No
of the world 3 0.15 % No
loading? false values 3 0.15 % No
to the spec 3 0.15 % No
the view code 3 0.15 % No
computer is a 3 0.15 % No
reframesubscribe modeltypeahead testing 2 0.10 % No
reframedispatch modelonquery foo 2 0.10 % No
modelonquery foo is 2 0.10 % No
foo is = 2 0.10 % No

SEO Keywords (Four Word)

Keyword Occurrence Density Possible Spam
state of the world 3 0.15 % No
suggestions loading? false values 3 0.15 % No
for the user to 2 0.10 % No
server for some suggestions 2 0.10 % No
q callback is = 2 0.10 % No
_ q callback is 2 0.10 % No
conforms to the spec 2 0.10 % No
fn _ _ q 2 0.10 % No
serverfetchsuggestions fn _ _ 2 0.10 % No
what the user types 2 0.10 % No
we ask the server 2 0.10 % No
ask the server for 2 0.10 % No
the server for some 2 0.10 % No
reframeregeventfx serverfetchsuggestions fn _ 2 0.10 % No
screen for the user 2 0.10 % No
Team Careers Community Contact 2 0.10 % No
sense to the user 2 0.10 % No
to the user The 2 0.10 % No
t is svalid? speccomponent 2 0.10 % No
is svalid? speccomponent t 2 0.10 % No

Internal links in - juxt.pro

Home
JUXT: Delivering Innovation
Delivery
JUXT: How We Deliver
Training
JUXT: JUXT Training Courses
Compliance
JUXT: Compliance
Blog
JUXT: The JUXT Blog
Why JUXT?
JUXT: Why JUXT?
Why Clojure?
JUXT: Why Clojure?
Clojure In
JUXT: Clojure In - Reference Clojure case studies from across Europe
Tech Radar
JUXT: Radar
Library
JUXT: Delivering Innovation
edge
The Edge Manual
tick
tick
yada
The yada manual
About Us
JUXT: About Us
Clients
JUXT: Clients
Team
JUXT: JUXTers
Careers
JUXT: Join JUXT
Community
JUXT: Community
Contact
JUXT: Contact
OnTheMarket.com and JUXT
JUXT: Blog: OnTheMarket.com and JUXT
Trading Dashboards for Tier-One Banking
JUXT: Blog: Trading Dashboards for Tier-One Banking
Building a Bitemporal Data-Store
JUXT: Blog: Building a Bitemporal Data-Store
Deploying libraries with deps.edn
JUXT: Blog: Deploying libraries with deps.edn
Testable Clojurescript apps
JUXT: Blog: Testable Clojurescript apps
Just a techie?
JUXT: Blog: Just a techie?
Login
JUXT:

Juxt.pro Spined HTML


JUXT: Blog: Testable Clojurescript apps ☰ Services Home Delivery Training Compliance Resources Blog Why JUXT? Why Clojure? Clojure In Tech Radar Library Tech crux whet tick yadaWell-nighAbout Us Clients Team Careers Community Contact By Oliver Hine Testable Clojurescript apps – Using re-frame, devcards and institute to build web apps on solid ground The world needs enlightenmentI was asked recently by a colleague - let's undeniability him Stathis S for anonymity's sake... no wait, let's undeniability him Mr S. Sideris - how I put front-end apps together. He wanted, in particular, to understand why he should consider using re-frame for a new project. I was in a similar situation a few years ago and without reading the re-frame introduction several times I still didn't quite get it so I went superiority without it, using just Reagent for rendering and my own lawmaking for managing all the data and state.This worked fine at first, but we had growing pains and I found that some repetitive vanilla lawmaking was emerging. At this point I went when to re-frame and this time it just clicked - it was the implementation we had been slowly unescapable ourselves, but properly thought-out and refined. For the things we have to learn surpassing we can do them, we learn by doing them - Aristotle It's not just well-nigh replacing our lawmaking with re-frame's, though - re-frame reverted the way I thought well-nigh the app, how it was structured and presented opportunities to make the tests better. I've had the good fortune to build four new apps for a vendee over the last three years and now would like to share the tideway refined over that time which promotes simple code, comprehensive testing and worthiness to scale (by which I midpoint add features) without it rhadamanthine a big tasty trencher of spaghetti.With whimsy as our staff and japery as our compass, let us begin.Gimme one visionImagine we are towers a typeahead. There are currently 614 typeahead libraries misogynist on Github, so what we need is a single universal typeahead library to supercede them all.A typeahead typically involves several things:Local state (what the user types)Data from the server (the suggestions that match what the user typed)An input box on the screen for the user to type intoA exhibit of suggestions on the screen for the user to selectWe would like the typeahead to squint something like the following, prejudiced as we are by the 614 typeahead implementations we've seen on our journey lanugo the infinite scroll of the internet. If we make ours squint widely similar people will once know how to use it - a diamond principle tabbed familiarity, if you're familiar with the term.Before we get going, you'll need the pursuit things:A Clojurescript project with Reagent, re-frame and Figwheel so we can develop with instant feedbackYour favourite browser all ready to fig and wheelAn empty washing up liquid bottleA ruler8 paperclipsYou can use the figwheel-main template to get the first two things. The others are misogynist on my eBay shop with discounted prices for readers of this blog post - use $DISCOUNT_CODE to get 20% off.Isolate the viewYou might think of your using stuff used as follows:And you could test it like this:There are problems here:Delving into nested HTML structures to make assertions is complicated, difficult to read, prone to breaking and still doesn't tell you if the exhibit makes any sense to the user - a CSS matriculation might render the whole thing invisible!To make the UI exhibit what we want we have to "drive" it with clicks and typing - very wearying to reach a deep corner caseIt's nonflexible to make assertions on the photons that might hit the user's eyeball, a complicated and mercurial API with millions of years of hacks, workarounds, new features and bugs and no decent driversIf the server starts returning variegated data we have no way of knowing if our app will breakWe would like to separate the eyebally bit out and sandbox it so that it doesn't pollute the rest of our code, which deals with pure computery data. Let's split the UI into two parts - the data model (mmmm, fresh!) and the view (full of the sticky goo found in eyeballs) which has only one job - representing the data to the user.The specThe boundaries in our lawmaking are a good place to test - the model deals purely with data and is testable by normal ways and the view deals purely with HTML and requires a variegated testing methodology. It therefore makes sense to introduce checks at the purlieus between these two, where the output of the model is consumed by the view. I can pinpoint this as a spec:Which will squint something like this:(s/def ::query (s/nilable string?)) (s/def ::loading? boolean?) (s/def ::value string?) (s/def ::values (s/coll-of ::value)) (s/def ::suggestions (s/nilable (s/keys :req-un [::loading?] :opt-un [::values]))) (s/def ::component (s/keys :req-un [::query ::suggestions])) Then I can build my model so that it outputs this data and my view so that it consumes this data.A modelThe re-frame documentation has colourful descriptions on how data flows like the water cycle. Your state of the world is used to render your app. The user, seeing the app, interacts with it and generates an event. The event is folded into the current state of the world to produce the next state of the world, which renders an updated view to the user and they can interact further. The whole trundling goes on forever, consuming events and rendering updates, growing like a woebegone slum engulfing the universe, destabilising unshortened galaxies with its infinite gravity well and spewing radiation until sooner your Chrome tab runs out of memory.For our typeahead the water trundling / woebegone slum of death will deal with all the parts of our UI that don't involve goo.We can encode this spritz of data using re-frame's two main concepts - event handlers and subscriptions. If you are unfamiliar with these I suggest you first read the informative and entertaining re-frame documentation surpassing returning here to pick up where you left off. I'll work on some of my side projects in the meantime. Watches 2 episodes of BetterUndeniabilitySaul... Welcome back! Shall we begin?We have a few events that can occur: the user types something, we ask the server for some suggestions, the response from the server arrives and the user chooses a suggestion. Every time an event happens we store some data in the database. Note that all these events, with the exception of asking the server, happen asynchronously in real life.;; user types something (and we ask the server for some suggestions) (re-frame/reg-event-fx ::on-query (fn [{:keys [db]} [_ q]] (merge {:db (-> db (assoc-in [:typeahead :query] q) (assoc-in [:typeahead :suggestions] {:loading? true}))} (if (string/blank? q) {:dispatch [::on-suggestions []]} {:dispatch [::server/fetch-suggestions q [::on-suggestions]]})))) ;; server responds with some suggestions (re-frame/reg-event-db ::on-suggestions (fn [db [_ suggestions]] (assoc-in db [:typeahead :suggestions] {:loading? false :suggestions suggestions}))) ;; user chooses a suggestion (re-frame/reg-event-db ::on-choose (fn [db [_ suggestion]] (assoc-in db [:typeahead] {:query suggestion}))) Now we need a way to get the data out of our re-frame database so that our view can render it. That's what subscriptions are for:(re-frame/reg-sub ::suggestions (fn [db [_ query]] (get-in db [:typeahead :suggestions]))) (re-frame/reg-sub ::query (fn [db [_ query]] (get-in db [:typeahead :query]))) (re-frame/reg-sub ::typeahead (fn [] [(re-frame/subscribe [::query]) (re-frame/subscribe [::suggestions])]) (fn [[query suggestions]] {:query query :suggestions suggestions})) Now that all our data eggs are in the re-frame basket we can start to write meaningful tests.Model testsWe can use our spec to predicate that, regardless of what the user types in or what the server returns, the main ::typeahead subscription unchangingly returns data that satisfies it.(defn- stub-server-response [expected-request response] (re-frame/reg-event-fx ::server/fetch-suggestions (fn [_ [_ q callback]] (is (= expected-request q)) {:dispatch (conj callback response)}))) (deftest querying-test (run-test-sync (stub-server-response "foo" ["bar"]) (let [t (re-frame/subscribe [::model/typeahead])] (testing "user types something" (re-frame/dispatch [::model/on-query "foo"]) (is (= {:query "foo" :suggestions {:loading? false :values ["bar"]}} @t)) (is (s/valid? ::spec/component @t)))))) This demonstrates not only that my model lawmaking outputs data that fits the spec, but moreover makes a hard-to-test asynchronous undeniability to the server behave synchronously using the stub. This is a full-length of day8/re-frame-test and is really useful in making complicated models hands testable. It moreover lets me test the transient state of the data stuff requested but no response received:(deftest loading-suggestions-test (run-test-sync (re-frame/reg-event-fx ::server/fetch-suggestions (fn [_ [_ q callback]] (is (= "foo" q)) ;; do not stimulation callback in order to observe the state surpassing the server has returned a response {})) (let [t (re-frame/subscribe [::model/typeahead])] (testing "user types something" (re-frame/dispatch [::model/on-query "foo"]) (is (= {:query "foo" :suggestions {:loading? true}} @t)) (is (s/valid? ::spec/component @t)))))) Tests don't need to run asynchronously with complicated locking or suffer from race conditions. Now we have covered the pursuit parts in testing:The gooThe view is where our data meets a random load of HTML which will hopefully present the data in a way that makes sense to the user. The weightier way of testing that is to just render something and have a squint at it! Let's write some Reagent lawmaking to manifest our vision in html tag form:(defn- suggestions [{:keys [loading? values] :as any?}] (when any? [:div.suggestions (if loading? [:div.loading "Loading suggestions..."] (if (seq values) [:ul (map-indexed (fn [i v] [:li {:key (str i v) :on-click #(re-frame/dispatch [::model/on-choose v])} v]) values)] "No suggestions found, please try flipside search"))])) (defn typeahead-component [model] [:div.typeahead [:input {:type "text" :value (:query model) :placeholder "Start typing to see suggestions" :on-change #(re-frame/dispatch [::model/on-query (.. % -target -value)])}] [suggestions (:suggestions model)]]) (s/fdef typeahead-component :args (s/cat :model ::spec/component)) (defn typeahead [] (let [model (re-frame/subscribe [::model/typeahead])] (fn [] [typeahead-component @model]))) Notice how simple it is - no waffly of the data as it gets passed around, just pure transforming functions to represent the data directly in HTML.View testsI use devcards to exercise the view code, and for assertions I use my gooey eyes. Devcards makes this practical considering it allows me to render many permutations of data, so that I can build a screen showing all my visual whet cases in one go. I know I am using realistic data in these cards considering I can trammels it versus my spec, and I can ensure this by turning on instrumentation so that it will wrack-up up if my test data drifts from the spec.(defcard-rg typeahead [:div [:h1 [:i "Initial state"]] [typeahead* {:query nil :suggestions nil}] [:h1 [:i "Loading suggestions"]] [typeahead* {:query "my computer is a" :suggestions {:loading? true}}] [:h1 [:i "Some suggestions"]] [typeahead* {:query "my computer is a" :suggestions {:loading? false :values ["My computer is vicarial weird" "My computer is a potato"]}}] [:h1 [:i "No suggestions"]] [typeahead* {:query "bz" :suggestions {:loading? false :values []}}]]) I can now go one step remoter and try to render randomly generated data using the spec:(defcard-rg generated-typeahead (let [models (gen/sample (s/gen ::spec/component))] [:div (map-indexed (fn [i m] [:div {:key i} [:pre (pr-str m)] [typeahead* m]]) models)])) I quickly found that I had React problems with indistinguishable keys if the same suggestion appeared multiple times. It moreover forced me to consider what to do if a suggestion was an empty string, considering that looks really weird:With some fixes in place for that, I now know thatmy model unchangingly outputs data conforming to my specmy view can properly exhibit any data that conforms to the specAutomatic for the peopleReviewing devcards by eye surpassing a release isn't too onerous, but it's still easy to forget and then harder still to track lanugo which transpiration tapped the UI. Automating this step is the natural way to go, and with a combination of chrome headless and imagemagick it's possible to capture screenshots and unequal devcards versus reference versions. I have an implementation of this which I hope to make unshut source in the future.998... 999...The final pieces of the puzzle are at the server/model boundary. If we knew what sort of data the server might return then we could prove our model can handle it, and if our model can handle it we know our UI can exhibit it.Some API technologies are self-describing - among them GraphQL and Swagger (OpenAPI). I have written data generation libraries for both of these: lacinia-gen and martian-test respectively, permitting you to generate data that your server will return as an input to your model tests.We now know thatWhatever data our server generates (or the user provides), our model can winnow as inputWhatever its input, our model can output data conforming to the specIf the data conforms to the spec, our view lawmaking can exhibit it correctlyLeading to proof by transitive relation that we can render everything properly, our using works as expected and we can add "full stack testing" to our CV.As a bonus, we moreover know that:If the server API changes we can run our model tests to find out if we need to make changesIf we do need to transpiration our model lawmaking we won't need to touch the view codeIf we just want to transpiration how some data is represented we only need to transpiration the view codeAhh, the bonus, the deep bonus. I don't know if you heard me counting, I did over a thousand.ConclusionWith some separation of concerns, introduction of lawmaking boundaries and assertions made at each purlieus we can modernize the way we build and test our apps. It's a bit like how a ship is built - each compartment is watertight and small unbearable that if any one of them is breached it can be sealed off to stop the ship sinking.On that rather apt metaphor for software development, I'll just recap on the libraries and tools used:View it: Reagent, devcardsModel it: re-frame, re-frame-testGen it: martian (blog post), lacinia-gen (blog post)Reload it - figwheel-main templateTechnologicThe lawmaking examples are from a toy project tabbed re-partee that I created to demonstrate the principles in this blog post. Published: 2018-11-22 Sign up to the JUXT newsletter Submit Privacy policy Through the looking graph Experiences raising GraphQL on a Clojure/script project by Oliver Hine Published: 2018-03-02 Securing your clojurescript app Use buddy's json tokens to demonstrate your single page using by Frankie Sardo Published: 2015-07-09 Datalog for trees in Clojure How to use Datalog in Clojure to match and pericope information from trees by Stathis Sideris Published: 2015-10-20 Copyright © JUXT LTD 2012-2018 Technology House, 151 Silbury Blvd., Milton Keynes MK9 1LH United Kingdom Company registration: 08457399 Home Software Clojure In Clojure RadarUnshutSource Software Services TrainingWell-nighUs Team Careers Community Contact In London Library Blog GitHub @juxtpro Login