Upgrade to Pro — share decks privately, control downloads, hide ads and more …

re-frame à la spec

re-frame à la spec

Introduction to ClojureScript SPA framework "re-frame" and its integration with clojure.spec.

Kent OHASHI

March 29, 2018
Tweet

More Decks by Kent OHASHI

Other Decks in Programming

Transcript

  1. Self-introduction /laʒenɔʁɛ̃k/ カマイルカ lagénorhynque (defprofile lagénorhynque :name "Kent OHASHI" :languages

    [Clojure Haskell Python Scala English français Deutsch русский] :interests [programming language-learning mathematics] :contributing [github.com/japan-clojurians/clojure-site-ja])
  2. React Reagent JSX (React element) Hiccup-like DSL React component ClojureScript

    function props args of component function state reagent.core/atom cf. Reagent ClojureScript & ReagentでReact⼊⾨してみた
  3. re-frame's data ow (a.k.a. "dominoes") 1. event dispatch 2. event

    handling 3. effect handling 4. query 5. view 6. DOM
  4. 1. event dispatch dispatch synchronously to [::events/initialize-db] (defn ^:export init

    [] (re-frame/dispatch-sync [::events/initialize-db]) (dev-setup) (mount-root)) src/cljs/rock_paper_scissors/core.cljs#L19-L22
  5. 2. event handling event handler for ::initialize-db (re-frame/reg-event-db ::initialize-db (fn

    [_ _] db/default-db)) src/cljs/rock_paper_scissors/events.cljs#L6-L9
  6. 3. effect handling effect handler for :db (reg-fx :db (fn

    [value] (if-not (identical? @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166
  7. 4. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _]

    (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8
  8. 5. view subscribe to [::subs/scene] (defn main-panel [] (case @(re-frame/subscribe

    [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33
  9. 1'. event dispatch dispatch to [::events/next-game] (defn start-button [label] [:input

    {:type "button" :on-click #(re-frame/dispatch [::events/next-game]) :value label}]) src/cljs/rock_paper_scissors/views.cljs#L8-L11
  10. 2'. event handling event handler for ::next-game (re-frame/reg-event-db ::next-game (fn

    [db _] (assoc db :scene ::db/now-playing))) src/cljs/rock_paper_scissors/events.cljs#L11-L14
  11. 3'. effect handling effect handler for :db (reg-fx :db (fn

    [value] (if-not (identical? @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166
  12. 4'. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _]

    (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8
  13. 5'. view subscribe to [::subs/scene] (defn main-panel [] (case @(re-frame/subscribe

    [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33
  14. 1''. event dispatch dispatch to [::events/select-your-hand h] (defn hands []

    [:div (map (fn [h] ^{:key h} [:input {:type "button" :on-click #(re-frame/dispatch [::events/select-your-hand h]) :value h}]) [::rps/rock ::rps/paper ::rps/scissors])]) src/cljs/rock_paper_scissors/views.cljs#L13-L19
  15. 2''. event handling event handler for ::events/select-your-hand (re-frame/reg-event-fx ::select-your-hand [(re-frame/inject-cofx

    ::cofx/select-enemy-hand)] (fn [{:keys [db enemy-hand]} [_ h]] {:db (assoc db :you h :enemy enemy-hand :scene ::db/over)})) src/cljs/rock_paper_scissors/events.cljs#L16-L23
  16. 3''. effect handling coeffect handler for ::select-enemy-hand (re-frame/reg-cofx ::select-enemy-hand (fn

    [cofx _] (assoc cofx :enemy-hand (rps/->hand (rand-int 3))))) src/cljs/rock_paper_scissors/cofx.cljs#L5-L9
  17. effect handler for :db (reg-fx :db (fn [value] (if-not (identical?

    @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166
  18. 4''. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _]

    (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8
  19. subscription for ::you-enemy subscription for ::you-enemy-hands (re-frame/reg-sub ::you-enemy (fn [db

    _] (select-keys db [:you :enemy]))) src/cljs/rock_paper_scissors/subs.cljs#L10-L13 (re-frame/reg-sub ::you-enemy-hands :<- [::you-enemy] (fn [{:keys [you enemy]} _] (str (name you) "(YOU) VS " (name enemy) "(ENEMY)"))) src/cljs/rock_paper_scissors/subs.cljs#L15-L19
  20. subscription for ::fight-result (re-frame/reg-sub ::fight-result :<- [::you-enemy] (fn [{:keys [you

    enemy]} _] (rps/fight you enemy))) src/cljs/rock_paper_scissors/subs.cljs#L21-L25
  21. subscription for ::result-color (re-frame/reg-sub ::result-color :<- [::fight-result] (fn [r _]

    (case r ::rps/win "red" ::rps/lose "blue" ::rps/draw "gray"))) src/cljs/rock_paper_scissors/subs.cljs#L27-L34
  22. 5''. view subscribe to [::subs/scene] subscribe to [::subs/you-enemy-hands] (defn main-panel

    [] (case @(re-frame/subscribe [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33
  23. subscribe to [::subs/fight-result] subscribe to [::subs/result-color] (defn result [] (let

    [r @(re-frame/subscribe [::subs/fight-result])] [:h1 {:style {:color @(re-frame/subscribe [::subs/result-color])}} r])) src/cljs/rock_paper_scissors/views.cljs#L21-L24
  24. split directories/namespaces for specs directory structure rock-paper-scissors ├── specs │

    └── cljs │ └── rock_paper_scissors │ └── * │ └── specs.cljs ├── src │ └── cljs │ └── rock_paper_scissors │ └── *.cljs └── test └── cljs └── rock_paper_scissors ├── *_test.cljs └── runner.cljs
  25. use clojure.spec in development only cljsbuild settings :cljsbuild {:builds [{:id

    "dev" :source-paths ["src/cljs" "specs/cljs"] ,,,} {:id "min" :source-paths ["src/cljs"] ,,,} {:id "test" :source-paths ["src/cljs" "specs/cljs" "test/cljs"] ,,,} ]} project.clj#L40-L70
  26. spec domain logic specs for rock-paper-scissors data (s/def ::hand #{::rps/rock

    ::rps/paper ::rps/scissors}) (s/def ::hand-num (s/int-in 0 3)) (s/def ::result #{::rps/win ::rps/lose ::rps/draw}) specs/cljs/rock_paper_scissors/rps/specs.cljs#L5-L9
  27. specs for rock-paper-scissors functions (s/fdef rps/<-hand :args (s/cat :hand ::hand)

    :ret ::hand-num) (s/fdef rps/->hand :args (s/cat :num ::hand-num) :ret ::hand) (s/fdef rps/fight :args (s/cat :you ::hand :enemy ::hand) :ret ::result) specs/cljs/rock_paper_scissors/rps/specs.cljs#L11- L22
  28. spec db data (s/def ::you ::rps.specs/hand) (s/def ::enemy ::rps.specs/hand) (s/def

    ::scene #{::db/start ::db/now-playing ::db/over}) (s/def ::db (s/keys :req-un [::you ::enemy ::scene])) specs/cljs/rock_paper_scissors/db/specs.cljs#L6- L12
  29. testing policy test domain logic test events via dispatch test

    subscriptions via subscribe do not test views
  30. test domain logic example-based tests with specs instrumented (t/use-fixtures :once

    {:before #(stest/instrument)}) (t/deftest test-fight (t/testing "rock-paper-scissors" (t/is (= ::sut/win (sut/fight ::sut/rock ::sut/scissors))) (t/is (= ::sut/lose (sut/fight ::sut/scissors ::sut/rock))) (t/is (= ::sut/draw (sut/fight ::sut/paper ::sut/paper))))) test/cljs/rock_paper_scissors/rps_test.cljs#L10-L17
  31. property-based tests using specs (tc/defspec prop-test-fight 1000 (let [fspec (s/get-spec

    #'sut/fight)] (prop/for-all [[you enemy] (-> fspec :args s/gen)] (s/valid? (:ret fspec) (sut/fight you enemy))))) test/cljs/rock_paper_scissors/rps_test.cljs#L33-L38
  32. test events and subscriptions (t/use-fixtures ; instrument specs :once {:before

    #(stest/instrument)}) (defn test-fixtures [] (re-frame/reg-fx ; mock :db effect :db (fn [value] ; validate db with specs (when-not (s/valid? ::db.specs/db value) (throw (ex-info "db spec check failed" (s/explain-data ::db.specs/db value)))) (if-not (identical? @app-db value) (reset! app-db value))))) test/cljs/rock_paper_scissors/events_test.cljs#L16- L26
  33. (t/deftest test-initialize-db (re-frame.test/run-test-sync (test-fixtures) (re-frame/dispatch [::sut/initialize-db]) (t/is ::db/start @(re-frame/subscribe [::subs/scene]))

    (t/is {:you ::rps/rock :enemy ::rps/rock} @(re-frame/subscribe [::subs/you-enemy])))) test/cljs/rock_paper_scissors/events_test.cljs#L28- L35
  34. (t/deftest test-select-your-hand (re-frame.test/run-test-sync (test-fixtures) (re-frame/reg-cofx ; mock ::cofx/select-enemy-hand coeffect ::cofx/select-enemy-hand

    (fn [cofx _] (assoc cofx :enemy-hand ::rps/rock))) (re-frame/dispatch [::sut/initialize-db]) (t/testing "draw" (re-frame/dispatch [::sut/next-game]) (re-frame/dispatch [::sut/select-your-hand ::rps/rock]) (t/is ::db/over @(re-frame/subscribe [::subs/scene])) (t/is "rock(YOU) VS rock(ENEMY)" @(re-frame/subscribe [::subs/you- (t/is ::rps/draw @(re-frame/subscribe [::subs/fight-result])) (t/is "gray" @(re-frame/subscribe [::subs/result-color]))))) test/cljs/rock_paper_scissors/events_test.cljs#L44- L73
  35. re-frame x clojure.spec unobtrusive use of clojure.spec only in development

    in separate directory/file/namespace focus on domain logic and db
  36. A little effort to write specs and tests can make

    our ClojureScript frontend life much happier!
  37. Reagent : A minimalistic ClojureScript interface to React.js reagent-project/reagent Guide

    to Reagent ClojureScript & ReagentでReact⼊⾨してみた - Qiita
  38. re-frame : A Reagent Framework For Writing SPAs, in Clojurescript.

    : Cross platform (cljs and clj) utilities for testing re-frame applications : A debugging dashboard for re-frame epochs. Comes with free x-ray glasses. Day8/re-frame Day8/re-frame-test Day8/re-frame-10x Re-frame: The Guide to Building Blocks
  39. clojure.spec clojure.spec - Rationale and Overview clojure.spec - 論理的根拠と概要 spec

    Guide やってみる!clojure.spec Spectacular Future with clojure.spec