A way of testing views in ClojureScript Apps

Francesco Vitullo
4 min readApr 20, 2019

--

Photo by Samuel Zeller on Unsplash

Recently, I have been embracing Clojure and ClojureScript since it’s fascinating a lot to me, so I consider myself at the beginning of a really long path.

I have tried to assemble a Front End stack that is appealing to me and I struggled to find examples on testing component rendering through UI (I find it a really good practice — and strongly suggest it), and given the fact I am a TDD/BDD fan, I decided to give it a try on my own.

These examples are based on Re-frame and Reagent (orchestrated by Shadow-cljs — really nice concept — ), focusing mostly on simple component rendering (stateless and stateful ones).

Since Re-frame and Reagent are based on React, I decided to use React-testing-library which helps (or force) to transition from test cases addressing UI structure to (almost pure) End-user approach (or BDD I’d say).

NPM devDependencies in the package.json look like the following:

"devDependencies": {
"karma": "^4.1.0",
"karma-chrome-launcher": "^2.2.0",
"karma-cljs-test": "^0.1.0",
"shadow-cljs": "^2.8.31",
"react-testing-library": "^6.1.2"
},

And below you can find the components we will use for the example:

(ns demo.components
(:require [reagent.core :as r]))

(
defn title-component [text] ;; Stateless component
[
:div
[:h1 text]])

(
defn counter-component [props] ;; Stateful component
(
let [counter (r/atom 0)]
(
fn [props]
[
:div
[:button {:on-click #(swap! counter inc)} (:text props)]
[
:p (str "Counter: " @counter)]])))

As you noticed, we have a simple component which returns a simple heading (rendering a passed in text) into a wrapped in a div, while on the bottom part, a stateful component which updates itself on user clicks (a simple counter component which increases a count on user clicks).

These are basically the majority of components we write so how do you we test them?

Let’s start importing the things we require in our test:

(ns demo.core-test
(:require [cljs.test :refer [deftest
testing
is
use-fixtures]]
[
clojure.string :refer [lower-case]]
[
demo.components :refer [title-component
counter-component]]
[
reagent.core :as r]
[
"react-testing-library" :as rtl])) ;; shadow-cljs helps

In our test we import the test suite from cljs.test, our components from the file we created, reagent.core and “react-testing-library” from the NPM ecosystem.

In order to avoid memory leaks and bad stuff, we will use the “cleanup” method provided by “react-testing-library” which will take care of rendered components.

(use-fixtures :each
{:after rtl/cleanup})

Afterward, let’s focus on testing our stateless component, “title-component”.

The first thing we might check, it’s that a title is containing in a heading1 tag:

(deftest test-title
(testing "The provided title should be enclosed in a h1"
(is
(=
"h1"
(-> (r/as-element [title-component "hello"]) ;; 1
(rtl/render) ;; 2

(.getByText "hello") ;; 3
(
.-tagName) ;; 4
(
lower-case))))) ;;5

Let’s describe the taken steps:

  1. It converts the component in a React element using the provided method (“as-element”) by Reagent;
  2. It renders the component with “react-testing-library” method “render’;
  3. It finds the element satisfying the text search “hello” (yes it might be a Regex too);
  4. When found, it extracts the “tagName” from the element;
  5. It just put it in a lower-case format;

In the end, we just check if the provided title/text is enclosed in an h1.

Oh yeah! :)

Actually, it might be actually even easier than that:

(testing "A title saying 'hello' is shown to the user"
(is
(=
"hello"
(-> (r/as-element [title-component "hello"])
(rtl/render)

(.getByText "hello")
(
.-innerHTML)))))

It would just check for what the end-user sees: yes, a simple text, no matter the tag is and it would be.

So we tested our stateless component, what about the stateful ones, with a click button for instance? It’s possible to use the “react-testing-library” in many cases (plenty of examples on the official page) and interop with it from ClojureScript code.

Let’s test the “counter-component” since it’s a bit different now.

(deftest test-stateful-component
(testing "A counter should track clicks"
(let [component (rtl/render (r/as-element [counter-component "increase"]))]
(
is ;; Part 1
(=
"Counter: 0"
(->> (.getByText component #"Counter:")
(
.-innerHTML))))
;; Part 2
(.click rtl/fireEvent (.getByText component "increase"))
(
r/flush)
(is ;; Part 3
(=
"Counter: 1"
(->> (.getByText component #"Counter:")
(
.-innerHTML))))
(
.unmount component))))

I split this into 3 parts, after storing the rendered component in a scoped variable:

  1. It checks whether the element containing a simple text “Counter: #counter” is there and it actually checks that it’s “Counter: 0” (since that’s the initial state)
  2. It fires a click event on the button (with the text “increase”) and it also fires “flush” provided by Reagent, since our cycle is a bit detached from the actual React’s one
  3. Similar to the first step, but it checks that at this time it’s containing “Counter: 1” since we expect an increase by 1.

By only these three steps, we put our end-user in our Unit Tests. We don’t care what “atom”, “props” or whatever are passed to the component, we just want to test and make sure that replicating the exact steps, something right it’s shown to the end-user.

I believe this is super useful when working with lots of components that might change quickly (e.g. in a middle-sized team), and we don’t want to break critical features as they are thought at the beginning.

The complete testing scenario might look like this:

https://gist.github.com/cdbkr/f195d7fbb600fae9655f37e7b2b4813e

This is a totally different approach and you might argue this might have been tested differently (E2E with Cypress, Nightwatch, Robot, Selenium, etc…) but this approach gives faster feedback to the developers and it traps failures before releasing/committing some code.

That’s it!!

I would be glad to hear your opinion, so please, feel free to provide any feedback or alternatives that might help this process.

Cheers! :)

--

--

Francesco Vitullo
Francesco Vitullo

Written by Francesco Vitullo

Senior Front End Engineer @IBM — thoughts are my own / I’m a passionate developer and kitesurfer

Responses (1)