Record of experiments, readings, links, videos and other things that I find on the long road.
Registro de experimentos, lecturas, links, vídeos y otras cosas que voy encontrando en el largo camino.
Friday, June 30, 2017
Interesting Talk: "Theory of Constraints"
Monday, June 26, 2017
Second Open TDD Training in Barcelona (in spanish)
El mes pasado Luis y yo hicimos un curso abierto de TDD en Barcelona. Fue una edición muy interesante en la que probamos algunos cambios en el contenido del curso, y en la que participaron algunos conocidos de la comunidad de Barcelona.
En las últimas ediciones del curso, nos habíamos dado cuenta de que el ejercicio de outside-in TDD, la Bank Kata, que hacíamos el segundo día le estaba resultando muy difícil a los alumnos. En outside-in TDD se usan los dobles de prueba como herramienta de diseño y exploración de interfaces, lo cual resulta muy complicado cuando una persona aún no ha acabado de entenderlos y manejarlos con soltura.
Esta dificultad estaba suponiendo un obstáculo para que acabaran de entender los dobles de prueba. Por ese motivo, decidimos mover el ejercicio de outside-in TDD al principio de un curso más avanzado de TDD que estamos preparando, y hacer otro ejercicio más sencillo en su lugar que les ayudase a asimilar mejor los conceptos.
El nuevo ejercicio que elegimos fue la Coffee Machine Kata. Es una kata muy interesante que ya había probado en un dojo de SCBCN. Creemos que nuestro experimento funcionó bastante bien. Usando esta nueva kata se entiende de forma más gradual y menos traumática cómo y cuándo aplicar cada tipo de doble de prueba. Acabamos muy satisfechos con el resultado de nuestro pequeño experimento y el feedback que recibimos.
Esta edición fue la segunda que hacíamos en lo que va de año, y hubo más gente que en la edición anterior. Esto se debió en gran parte a que vinieron desde Zaragoza cuatro personas que trabajan en Inycom. Muchas gracias por confiar en nosotros.
También nos gustaría darle las gracias a todos los asistentes por su entrega y ganas de aprender. Finalmente, agradecer a Magento, especialmente a Ángel Rojo que nos hayan acogido de nuevo en sus oficinas y toda la ayuda que nos prestaron, y a nuestra compañera Dácil por organizarlo todo.
Publicado originalmente en el blog de Codesai.Friday, June 23, 2017
Testing Om components with cljs-react-test
This is the code of the test:
(ns horizon.controls.widgets.views-selector-tests.views-selector-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is use-fixtures async]] | |
[sablono.core :as sab :include-macros true] | |
[cljs-react-test.utils :as tu] | |
[cljs-react-test.simulate :as sim] | |
[cljs.core.async :as core.async] | |
[om.core :as om :include-macros true] | |
[horizon.controls.widgets.comboboxes.views-selector :as views-selector] | |
[horizon.common.messaging.core :as m] | |
[horizon.test-helpers.async-test-tools :as async-test-tools] | |
[horizon.test-doubles.core :as td :include-macros true] | |
[horizon.common.logging :as log] | |
[horizon.test-helpers.dom-selection :as dom-selection])) | |
(def ^:private ^:dynamic c) | |
(use-fixtures :each | |
{:before #(async done | |
(set! c (tu/new-container!)) | |
(done)) | |
:after #(do | |
(td/with-doubles | |
:ignoring [m/unsubscribe] | |
(tu/unmount! c)))}) | |
(def ^:private possible-views-options | |
[{:Id :view1 :Name "Custom View 1"} | |
{:Id :view2 :Name "Custom View 2"}]) | |
(defn- mount-component-on-root | |
[root-container combo-id-num app-state ui-events-channel commands-channel] | |
(td/with-doubles | |
:ignoring [log/debug] | |
(om/root | |
(fn [data owner] | |
(reify | |
om/IRenderState | |
(render-state [_ _] | |
(sab/html | |
[:div | |
(om/build | |
views-selector/views-combobox | |
data | |
{:opts {:channels {:ui-events-channel ui-events-channel | |
:commands-channel commands-channel} | |
:generate-id-fn (constantly combo-id-num)} | |
:init-state {:expanded true | |
:value {:selected (:selected @app-state)}}})])))) | |
app-state | |
{:target root-container}))) | |
(deftest selecting-save-current-view-action | |
(let [ui-events-channel (core.async/chan) | |
commands-channel (core.async/chan) | |
app-state (atom {:pre-label "Select view:" | |
:views-options possible-views-options | |
:selected -1})] | |
(mount-component-on-root | |
c 100 app-state ui-events-channel commands-channel) | |
(async | |
done | |
(async-test-tools/expect-async-message | |
commands-channel | |
:done-fn done | |
:expected-message {:type :command | |
:payload {:command :show-custom-view | |
:Id :view1}}) | |
(sim/click (dom-selection/nth-li c 2) {})))) | |
(deftest selecting-showing-first-custom-view-action | |
(let [ui-events-channel (core.async/chan) | |
commands-channel (core.async/chan) | |
app-state (atom {:pre-label "Select view:" | |
:views-options possible-views-options | |
:selected -1})] | |
(mount-component-on-root | |
c 100 app-state ui-events-channel commands-channel) | |
(async | |
done | |
(async-test-tools/expect-async-message | |
commands-channel | |
:done-fn done | |
:expected-message {:type :command | |
:payload {:command :save-current-view}}) | |
(sim/click (dom-selection/nth-li c 4) {})))) | |
(deftest deleting-view-action | |
(let [ui-events-channel (core.async/chan) | |
commands-channel (core.async/chan) | |
app-state (atom {:pre-label "Select view:" | |
:views-options possible-views-options | |
:selected -1})] | |
(mount-component-on-root | |
c 100 app-state ui-events-channel commands-channel) | |
(async | |
done | |
(async-test-tools/expect-async-message | |
commands-channel | |
:done-fn done | |
:expected-message {:type :command | |
:payload {:command :delete-custom-view | |
:Id :view2}}) | |
(sim/click (second (dom-selection/elements-of-class c "delete")) {})))) |
All the tests follow this structure:
- Setting up the initial state in an atom, app-state. This atom contains the data that will be passed to the control.
- Mounting the Om root on the container. Notice that the combobox is already expanded to save a click.
- Declaring what we expect to receive from the commands-channel using expect-async-message.
- Finally, selecting the option we want from the combobox, and clicking on it.
(ns horizon.test-helpers.async-test-tools | |
(:require | |
[cljs.test :refer-macros [is]] | |
[cljs.core.async :as core.async]) | |
(:require-macros | |
[cljs.core.async.macros :refer [go go-loop]])) | |
(def ^:private timeout-in-ms 2000) | |
(defn- channel-timed-out-error [received-messages] | |
(ex-info | |
"Channel timed out!!!" | |
{:cause :channel-timeout | |
:received-messages received-messages})) | |
(defn- maybe-exclude-keys [x ks] | |
(if (and (map? x) ks) | |
(apply dissoc x ks) | |
x)) | |
(defn- invoke! [f] | |
{:pre [(if f (ifn? f) true)]} | |
(when f | |
(f))) | |
(defn- check-result | |
[& {:keys [expected result done-fn excluding-keys] :as options}] | |
{:pre [(every? #{:expected :result :done-fn :excluding-keys} | |
(keys options)) | |
(if done-fn (ifn? done-fn) true)]} | |
(let [expected (maybe-exclude-keys expected excluding-keys) | |
result (maybe-exclude-keys result excluding-keys)] | |
(try | |
(is (= expected result)) | |
(finally | |
(invoke! done-fn))))) | |
(defn check-results | |
[& {:keys [expected result done-fn excluding-keys] :as options}] | |
{:pre [(every? #{:expected :result :done-fn :excluding-keys} | |
(keys options)) | |
(if done-fn (ifn? done-fn) true)]} | |
(try | |
(mapv #(check-result :expected %1 | |
:result %2 | |
:excluding-keys excluding-keys) | |
expected | |
result) | |
(finally | |
(invoke! done-fn)))) | |
(defn- fail-because-of-timeout | |
[expected-message received-messages done-fn] | |
(check-result :expected expected-message | |
:result (channel-timed-out-error received-messages) | |
:done-fn done-fn)) | |
(defn expect-async-message | |
[channel & {:keys [expected-message done-fn excluding-keys]}] | |
{:pre [expected-message (if done-fn (ifn? done-fn) true)]} | |
(go | |
(let [[message ch] (core.async/alts! | |
[channel (core.async/timeout timeout-in-ms)])] | |
(if (= ch channel) | |
(check-result :expected expected-message | |
:result message | |
:done-fn done-fn | |
:excluding-keys excluding-keys) | |
(fail-because-of-timeout expected-message [] done-fn))))) | |
(defn expect-async-message-with-specific-type | |
[channel & {:keys [expected-message-type expected-message done-fn excluding-keys]}] | |
{:pre [expected-message expected-message-type (if done-fn (ifn? done-fn) true)]} | |
(go-loop [received-messages []] | |
(let [[message ch] (core.async/alts! | |
[channel (core.async/timeout timeout-in-ms)])] | |
(if (= ch channel) | |
(if (= expected-message-type (:type message)) | |
(check-result :expected expected-message | |
:result message | |
:done-fn done-fn | |
:excluding-keys excluding-keys) | |
(recur (conj received-messages message))) | |
(fail-because-of-timeout expected-message received-messages done-fn))))) | |
(defn expect-n-async-messages | |
[channel & {:keys [expected-messages done-fn excluding-keys]}] | |
{:pre [(sequential? expected-messages) (if done-fn (ifn? done-fn) true)]} | |
(go-loop [received-messages []] | |
(let [[message ch] (core.async/alts! | |
[channel (core.async/timeout timeout-in-ms)]) | |
total (count expected-messages)] | |
(if (= ch channel) | |
(let [received-messages (conj received-messages message)] | |
(if (= total (count received-messages)) | |
(check-results :expected expected-messages | |
:result received-messages | |
:done-fn done-fn | |
:excluding-keys excluding-keys) | |
(recur received-messages))) | |
(fail-because-of-timeout expected-messages received-messages done-fn))))) | |
(defn async-used-fn [channel func-kw] | |
(fn [& args] | |
(go | |
(core.async/>! channel {:used func-kw :with-args (vec args)})))) | |
(defn send-fake-msg! [channel msg] | |
(go | |
(core.async/>! channel msg))) | |
(defn expect-event-handler-receives-args | |
[& {:keys [event channel expected-args done-fn close-fn]}] | |
{:pre [event | |
channel | |
expected-args | |
(if done-fn (ifn? done-fn) true) | |
(if close-fn (ifn? close-fn) true)]} | |
(let [events-channel (core.async/chan) | |
close-and-done-fn (fn [] | |
(when close-fn (close-fn channel)) | |
(when done-fn (done-fn channel)))] | |
(event | |
channel | |
(fn [& args] | |
(go | |
(core.async/>! events-channel args)))) | |
(go | |
(let [[args ch] (core.async/alts! | |
[events-channel (core.async/timeout timeout-in-ms)])] | |
(if (= ch events-channel) | |
(check-result | |
:expected expected-args :result args :done-fn close-and-done-fn) | |
(fail-because-of-timeout | |
expected-args :never-called close-and-done-fn)))))) |
Interesting Webcast: "CS Education Zoo interviews David Nolen"
Sunday, June 18, 2017
Interesting Talk: "Desorientados a objetos"
Saturday, June 17, 2017
Course: Introduction to CSS3 on Coursera
Sunday, June 4, 2017
Kata: Luhn Test in Clojure
This is a very interesting problem to practice TDD because it isn't so obvious how to test drive a solution through the only function in its public API: valid?.
What we observed during the dojo is that, since the implementation of the Luhn Test is described in so much detail in terms of the s1 and s2 functions (check its description here), it was very tempting for participants to test these two private functions instead of the valid? function.
Another variant of that approach consisted in making those functions public in a different module or class, to avoid feeling "guilty" for testing private functions. Even though in this case, only public functions were tested, these approach produced a solution which has much more elements than needed, i.e. with a poorer design according to the 4 rules of simple design. It also produced tests that are very coupled to implementation details.
In a language with a good REPL, a better and faster approach might have been writing a failing test for the valid? function, and then interactively develop with the REPL s1 and s2 functions. Then combining s1 and s2 would have made the failing test for valid? pass. At the end, we could add some other tests for valid? to gain confidence in the solution.
This mostly REPL-driven approach is fast and produces tests that don't know anything about the implementation details of valid?. However, we need to realize that, it follows the same technique of "testing" (although only interactively) private functions. The huge improvement is that we don't keep these tests and we don't create more elements than needed. However, the weakness of this approach is that it leaves us with less protection against possible regressions. That's why we need to complement it with some tests after the code is written to gain confidence in the solution.
If we use TDD writing tests only for the valid? function, we can avoid creating superfluous elements and create a good protection against regressions at the same time. We only need to choose our test examples wisely.
These are the tests I used to test drive a solution (using Midje):
(ns luhn-test.core-test | |
(:require [midje.sweet :refer :all] | |
[luhn-test.core :as luhn])) | |
(facts | |
"about luhn test" | |
(luhn/valid? "0") => true? | |
(luhn/valid? "1") =not=> true? | |
(luhn/valid? "18") => true? | |
(luhn/valid? "612") => true? | |
(luhn/valid? "1016") => true? | |
(luhn/valid? "4044") => true? | |
(luhn/valid? "91") => true? | |
(luhn/valid? "49927398716") => true? | |
(luhn/valid? "79927398713") => true? | |
(luhn/valid? "49927398712") =not=> true? | |
(luhn/valid? "79927398715") =not=> true?) |
This is the resulting code:
(ns luhn-test.core) | |
(def ^:private sum-digits #(+ (quot % 10) (mod % 10))) | |
(defn- double-when-at-even-position [position num] | |
(if (even? (inc position)) (* 2 num) num)) | |
(defn- reduce-digits [digits] | |
(->> digits | |
(reverse) | |
(map #(Integer/parseInt (str %))) | |
(map-indexed double-when-at-even-position) | |
(map sum-digits) | |
(apply +))) | |
(defn valid? [digits] | |
(zero? (mod (reduce-digits digits) 10))) |
We can improve this regression test suit by changing some of the tests to make them fail for different reasons:
(ns luhn-test.core-test | |
(:require [midje.sweet :refer :all] | |
[luhn-test.core :as luhn])) | |
(facts | |
"about luhn test" | |
(luhn/valid? "00000000000") => true? | |
(luhn/valid? "00000000001") =not=> true? | |
(luhn/valid? "00000000505") => true? | |
(luhn/valid? "00000000018") => true? | |
(luhn/valid? "00000002030") => true? | |
(luhn/valid? "00000000091") => true? | |
(luhn/valid? "49927398716") => true? | |
(luhn/valid? "79927398713") => true? | |
(luhn/valid? "49927398712") =not=> true? | |
(luhn/valid? "79927398715") =not=> true?) |