Tutorial 14 - Better Safe Than Sorry (Part 2)
February 2, 2017 · View on GitHub
To adhere to the progressive enhancement strategy in the latest tutorial we introduced Enlive by Christophe Grand and used it to implement the server-side-only version of the Shopping Calculator. In doing that implementation, we were forced to refactor the code for two reasons:
- to apply the DRY principle
- to resolve a cyclic namespaces dependency problem we met on the way.
In this tutorial we're going to introduce code testing.
Preamble
To start working from the end of the previous tutorial, assuming you've git installed, do as follows
git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-13
Introduction
Before introducing code testing we first want to accomplish a few other things:
- review the Shopping Form
- break the Shopping Form
- add field validations
Start the IFDE
As usual we first start the IFDE live environment
cd /path/to/modern-cljs
boot dev
...
Elapsed time: 23.261 sec
Review the Shopping Form
Now disable the JavaScript engine of your browser, visit or reload the
shopping URI, and click the Calculate button. The Total field
is filled with the result of the calculation executed on the
server-side via the "/shopping" action associated to the
shoppingForm. That's what we reached at the end of the
previous tutorial. So far so good.
Break the Shopping Calculator again and again
Now enter an unexpected value in the form, for example foo as the
value of the Price per Unit field. Finally, click the Calculate
button.
You'll receive the infamous HTTP ERROR: 500 page saying that
clojure.lang.Symbol can't be cast to java.lang.Number. This is a
java.lang.ClassCastException as you can read from the warning
reported at the terminal as well.
2015-12-05 10:49:28.421:WARN:oejs.HttpChannel:qtp540393068-80: /shopping
java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.Number
at clojure.lang.Numbers.multiply(Numbers.java:148)
at modern_cljs.remotes$calculate.invoke(remotes.clj:6)
...
at java.lang.Thread.run(Thread.java:745)
That's because the remote calculate function accepts only
stringified numbers for its calculation and we're now passing to it a
stringified symbol that can't be casted to a number.
Now let's see what happens if we reactivate the JavaScript engine and reload the Shopping Form URL.
Try again to type foo instead of a number in one of the form fields
and click Calculate after having reloaded the
Shopping From page.
This time, due to the Ajax communication, even though the server
returned the same 500 error code as before, the browser does not show
the same ERROR PAGE. The result is not as bad as before, but still
unacceptable for a professional web page.
Before we invest time and effort in unit testing, let's understand what the Shopping Form lacks?
It needs input validation: both for the server and the client sides, as usual.
Shopping Form validation
We already used Valip lib by Chas Emerick in the
Tutorial-12 - Don't Repeat Yourself while crossing the border -
to validate the loginForm fields, and we already know how to apply
the same approach to the shoppingForm validation.
We know from the above tutorial that we can
share
the validation rules between the server and the client sides by
creating a portable CLJ/CLJS source file with the .cljc extension in
the src/cljc source directory's structure.
validators.cljc
Create the directory shopping under the src/cljc/modern_cljs/
directory to reflect the project structure we already used for the
login validation.
mkdir src/cljc/modern_cljs/shopping
In the shopping directory now create the file validators.cljc
where we're going to define the validate-shopping-form function,
which uses the valip.core and valip.predicates namespaces.
touch src/cljc/modern_cljs/shopping/validators.cljc
NOTE 1: you'll get a
java.lang.AssertionErrorbecause there is nonsform in the newly created file. Don't worry. As soon as you'll save the edited file the error will disappear.
To keep things simple, at first we will consider only very basic validations:
- no input can be empty
- the value of
quantityhas to be a positve integer - the values of
price,taxanddiscounthave to be numbers
Following is the content of the newly created validators.cljc file.
(ns modern-cljs.shopping.validators
(:require [valip.core :refer [validate]]
[valip.predicates :refer [present?
integer-string?
decimal-string?
gt]]))
(defn validate-shopping-form [quantity price tax discount]
(validate {:quantity quantity :price price :tax tax :discount discount}
;; validate presence
[:quantity present? "Quantity can't be empty"]
[:price present? "Price can't be empty"]
[:tax present? "Tax can't be empty"]
[:discount present? "Discount can't be empty"]
;; validate type
[:quantity integer-string? "Quantity has to be an integer number"]
[:price decimal-string? "Price has to be a number"]
[:tax decimal-string? "Tax has to be a number"]
[:discount decimal-string? "Discount has to be a number"]
;; validate range
[:quantity (gt -0.1) "Quantity can't be negative"]
;; other specific platform validations (not at the moment)
))
NOTE 2: At the moment we don't deal with any issues regarding internationalization and we're hard-coding the text messages in the source code.
As you can see, we defined the validate-shopping-form function by
using the validate function from the valip.core namespace and a
bunch of predicates that Chas Emerick was so kind to have defined
for us in the valip.predicates namespace.
Considering that valip is a portable lib, we can immediately test the
validate-shopping-form function at the CLJ REPL and at the CLJS
bREPL as well.
The server side
Let's start from the server side
# in a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=>
Now exercise the newly defined validate-shopping-form function as
follows:
boot.user> (use 'modern-cljs.shopping.validators)
nil
boot.user> (validate-shopping-form "1" "0" "0" "0")
nil
boot.user> (validate-shopping-form "-10" "0" "0" "0")
{:quantity ["Quantity can't be negative"]}
boot.user> (validate-shopping-form "-10" "0" "0" "")
{:discount ["Discount can't be empty" "Discount has to be a number"], :quantity ["Quantity can't be negative"]}
The above REPL session testing the validate-shopping-form
function is not a substitute for unit testing, since the tests are
all manual and cannot be re-run without duplicating all of this work. Moreover,
considering that the above validate-shopping-form validator is
portable on CLJS as well, you would need to manually repeat them on the
client-side as well.
Unit testing
No programmer likes code repetition. Re-typing similar expressions again and again in the REPL is something that should feel awful to everyone.
Fortunately, both CLJ and CLJS offer a solution for automating
such boring activities. Unfortunately, unit testing on CLJ/JVM is
not exactly the same as unit testing on CLJS/JSVM. The two platforms are
different from one another and they require different namespaces:
the clojure.test namespace for CLJ and the cljs.test namespace for CLJS.
The clojure.test namespace was created long before the
corresponding cljs.test namespace and the latter, through a detailed
porting to the JSVM, preserved most of the functionalities provided by
the former. So we are still in a good position for writing unit tests
that are applicable to a portable CLJ/CLJS namespace like
modern-cljs.shopping.validators.
That said, most of the cljs.test functionalities are provided as
macros, which means that you must use the special CLJS :require-macros
option in the (ns ...) declaration of a testing namespace, while
you do not need it in the corresponding CLJ namespace declaration.
We have already seen an instance of this. Indeed, while writing the portable
modern-cljs.login.validators namespace, we needed to differentiate
between the CLJ/JVM platform and the CLJS/JSVM platform.
For that case we introduced the use of the #? reader literal. It
allowed us to make the email-domain-errors validator available on
the CLJ/JVM platform only while writing portable code in a cljc
source file.
To write a portable testing namespace for the portable
modern-cljs.shopping.validators namespace we created above, we have
to use the same #? trick to differentiate the different testing
namespace declarations used by CLJ and CLJS.
Mirroring the project structure
When creating unit tests, we want to host them in a different path from the application source files. Generally speaking you want to mimic in a test directory structure the same layout you created for the application source files.
Under the src main directory we currently have three subdirectories,
one for each source file extension: clj, cljs and cljc.
At the moment we'll mimic the same structure for the cljc directory
only, because we're going to create the unit tests for that
modern-cljs.shopping.validators namespace only.
mkdir -p test/cljc/modern_cljs/shopping
NOTE 3: Although you are not required to mimic the source directory layout in the test directory, doing so will make the project structure simpler and easier to understand.
We now need to create the unit test file for the
modern-cljs.shopping.validators namespace. You could name this file
anything, but I like to give it a name resembling the
source namespace file (i.e. validators), plus an indicator that it
defines unit tests. Following the pattern validators_test.cljc seems to
be a good name for our needs.
touch test/cljc/modern_cljs/shopping/validators_test.cljc
NOTE 4: This time you'll not receive any error regarding the file not containing an
nsform. This is becausebootstill does not know anything about thetestdirectory structure in which the newly created file lives.
You'll end up the following test structure
test
└── cljc
└── modern_cljs
└── shopping
└── validators_test.cljc
Start unit testing at the borders
The reason why we chose to start testing from the
modern-cljs.shopping.validators namespace is because it sits at the
border of our application with the external world: be it a user to be
notified about her/his mistyped input, or an attacker trying to
leverage any useful information by breaking the Shopping Calculator.
I prefer to speak about "testing namespaces", rather than "unit testing functions". Generally speaking you should unit test the API of each namespace, which means all of the symbols which are not private in the namespace itself. If you want to unit test a private symbol, you should write the unit test code in the same file where you defined the private symbol.
The first unit test
At the moment the modern-cljs.shopping.validators namespace contains
the validate-shopping-form function only, which is the one we're
going to unit test.
The following is the initial unit test code that mimics the short REPL
testing session we did previously for the
modern-cljs.shopping.validators namespace.
(ns modern-cljs.shopping.validators-test
(:require [modern-cljs.shopping.validators :refer [validate-shopping-form]]
#?(:clj [clojure.test :refer [deftest is]]
:cljs [cljs.test :refer-macros [deftest is]])))
(deftest validate-shopping-form-test
(is (= nil (validate-shopping-form "1" "0" "0" "0")))
(is (= nil (validate-shopping-form "1" "0.0" "0.0" "0.0")))
(is (= nil (validate-shopping-form "100" "100.25" "8.25" "123.45"))))
NOTE 5: We made the
modern-cljs.shopping.validators-testnamespace compatible with both CLJ and CLJS by using the#?reader literal. Depending on the available feature at compile-time,:cljof:cljs, the reader will cause the correct namespace to be required in thensdeclaration.
We have started very simply.
By using the deftest and the is macros, we defined our first unit
test named validate-shopping-form-test. As mentioned previously, while we are
free to name the tests anything we want, I prefer to adopt some conventions
to facilitate the writing and reading of unit tests. If I want to test a
function named my-function, I name the test my-function-test.
The is macro allows us to make assertions about arbitrary
expressions. The code (is (= nil (validate-shopping-form "1" "0" "0" "0"))) means that the evaluation of (validate-shopping-form "1" "0" "0" "0")
must return nil for the test to pass. For this test, all the input values
are valid (at the moment, we're just testing the happy path).
On getting less bored
You'll quickly get very bored typing is forms. That is why
clojure.test and cljs.test include the are macro which allows
the programmer to save some typing. The above group of is forms can
be reduced to the following equivalent are form:
(ns modern-cljs.shopping.validators-test
(:require [modern-cljs.shopping.validators :refer [validate-shopping-form]]
#?(:clj [clojure.test :refer [deftest are]]
:cljs [cljs.test :refer-macros [deftest are]])))
(deftest validate-shopping-form-test
(are [expected actual] (= expected actual)
nil (validate-shopping-form "1" "0" "0" "0")
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))
NOTE 6: don't forget to substitute
iswitharein the:refer/:refer-macrossections of the requirements.
In the above example, each nil value represents an expected value,
while the evaluation of each validate-shopping-form call represents
the actual value. The are macro verifies that for each pair of an
expected and actual values [expected actual], the assertion (= expected actual) is true.
How to read failure reports
You can even document tests by wrapping your are form inside a
testing macro, which takes a documentation string followed by any
number of is/are assertions.
(ns modern-cljs.shopping.validators-test
(:require [modern-cljs.shopping.validators :refer [validate-shopping-form]]
#?(:clj [clojure.test :refer [deftest are testing]]
:cljs [cljs.test :refer-macros [deftest are testing]])))
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "1" "0" "0" "0")
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45"))))
NOTE 7: Again, don't forget to add the
testingsymbol in the:refer/:refer-macrossections of the requirements.
The string "Shopping Form Validation" will be included in failure
reports. Calls to testing macros may even be nested to allow better
reporting of failure reports.
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(testing "/ Happy Path"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "1" "0" "0" "0")
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))))
Running the tests
To run the newly defined unit test for the
modern-cljs.shopping.validators namespace, we have some options. The
one we're going to use right now does not require killing the running
boot process even though we're going to alter the boot environment
established by the build.boot file when we started IFDE with the
boot dev command.
This is the first time in the series that we do not stop boot to
alter its runtime environment. Sometimes this is useful,
especially when you're experimenting with something new (as
we are now).
Dynamically alter the boot environment
In the previous paragraphs we created the
test/cljc/modern_cljs/shopping directory. Then we created the
validators_test.cljc source file for unit testing the
modern-cljs.shopping.validators namespace. At the moment boot does
not know anything about this new source file, because the
:source-paths environment variable in the build.boot file
has been set to #{"src/clj" "src/cljs" "src/cljc"}.
There is a pretty easy way to dynamically add a new directory to
:source-paths at the REPL:
boot.user> (set-env! :source-paths #(conj % "test/cljc"))
nil
If you're curious about the set-env! function, you can examine its docstring:
boot.user> (doc set-env!)
-------------------------
boot.core/set-env!
([& kvs])
Update the boot environment atom `this` with the given key-value pairs given
in `kvs`. See also `post-env!` and `pre-env!`. The values in the env map must
be both printable by the Clojure printer and readable by its reader. If the
value for a key is a function, that function will be applied to the current
value of that key and the result will become the new value (similar to how
clojure.core/update-in works.
nil
As you can see, we fit the last case. The value we passed for the
:source-paths key is an anonymous function to conj the
"test/cljc" source directory to the previous value represented by
the % symbol.
In this way you dynamically add the test/cljc directory to the
:source-paths environment variable of boot. Thus,
boot now knows about the new modern-cljs.shopping.validator-test namespace
we declared above.
Light the fire on the server side
While we're in the CLJ REPL, we can require the clojure.test and the
modern-cljs.shopping.validators-test namespaces
boot.user> (require '[clojure.test :as t]
'[modern-cljs.shopping.validators-test])
nil
and then run the server-side unit tests for the shopping's fields validation as follows:
boot.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
{:test 1, :pass 3, :fail 0, :error 0, :type :summary}
So far, so good.
Break the test
To see how clojure.test reports failures, let's now try to modify
an assertion to produce a failure.
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(testing "/ Happy Path"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "" "0" "0" "0") ;; produce a failure
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))))
Now re-evaluate the above the above run-tests expression
boot.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
{:test 1, :pass 3, :fail 0, :error 0, :type :summary}
Oops, nothing new happens. The unit test succeeded again. The problem
is that even if the validate-shopping-form-test function got
redefined and recompiled, clojure.test still knows about the old
definition. To obtain the expected effect we have to explicitly reload
the modern-cljs.shopping.validators-test namespace and re-run
run-tests.
boot.user> (require '[modern-cljs.shopping.validators-test] :reload)
nil
boot.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (validators_test.cljc:8)
Shopping Form Validation / Happy Path
expected: (= nil (validate-shopping-form "" "0" "0" "0"))
actual: (not (= nil {:quantity ["Quantity can't be empty" "Quantity has to be an integer number" "Quantity can't be negative"]}))
Ran 1 tests containing 3 assertions.
1 failures, 0 errors.
{:test 1, :pass 2, :fail 1, :error 0, :type :summary}
Now we're talking. Even if the failure report does require a bit of interpretation at first, with some practice you can understand it more quickly.
Now revert the above failing test to its proper form, reload its namespace and
rerun run-tests so everything is passing again.
boot.user> (require '[modern-cljs.shopping.validators-test] :reload)
nil
boot.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
{:test 1, :pass 3, :fail 0, :error 0, :type :summary}
Do not cover only the working path
The tests we have written so far include only a few assertions about the happy path
in validate-shopping-form. Generally speaking you should
also cover the alternative/exception paths. Let's add few of them in
our first test.
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(testing "/ Happy Path"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "1" "0" "0" "0")
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))
(testing "/ No presence"
(are [expected actual] (= expected actual)
"Quantity can't be empty"
(first (:quantity (validate-shopping-form "" "0" "0" "0")))
"Price can't be empty"
(first (:price (validate-shopping-form "1" "" "0" "0")))
"Tax can't be empty"
(first (:tax (validate-shopping-form "1" "0" "" "0")))
"Discount can't be empty"
(first (:discount (validate-shopping-form "1" "0" "0" "")))))
(testing "/ Value Type"
(are [expected actual] (= expected actual)
"Quantity has to be an integer number"
(first (:quantity (validate-shopping-form "foo" "0" "0" "0")))
"Quantity has to be an integer number"
(first (:quantity (validate-shopping-form "1.1" "0" "0" "0")))
"Price has to be a number"
(first (:price (validate-shopping-form "1" "foo" "0" "0")))
"Tax has to be a number"
(first (:tax (validate-shopping-form "1" "0" "foo" "0")))
"Discount has to be a number"
(first (:discount (validate-shopping-form "1" "0" "0" "foo")))))
(testing "/ Value Range"
(are [expected actual] (= expected actual)
"Quantity can't be negative"
(first (:quantity (validate-shopping-form "-1" "0" "0" "0")))))))
We organized the assertions by using the nesting feature of the
testing macro to reflect the kind of validations implemented in the
validate-shopping-form function.
Even though we could add more assertions, for the moment the coverage of
the modern-cljs.shopping.validators namespace is enough to grasp the
idea of the clojure.test mechanics.
To run above unit tests do as before. First reload the namespace containing the test, then run the tests:
boot.user> (require '[modern-cljs.shopping.validators-test] :reload)
nil
boot.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
{:test 1, :pass 13, :fail 0, :error 0, :type :summary}
As you can see, we have one test containing 13 assertions, and all of them succeed.
The magic
We now want to repeat the magic we have already seen at work in a previous
tutorial dedicated to the loginForm.
Start the CLJS bREPL from the the CLJ REPL as usual
boot.user> (start-repl)
<< started Weasel server on ws://127.0.0.1:49522 >>
<< waiting for client to connect ... Connection is ws://localhost:49522
Writing boot_cljs_repl.cljs...
connected! >>
To quit, type: :cljs/quit
nil
cljs.user>
Require the needed namespaces
First require the cljs.test namespace
cljs.user> (require '[cljs.test :as t :include-macros true])
nil
As you see we had to use the :include-macros option keyword to include
the macros.
Then we have to require modern-cljs.shopping.validators-test
namespace containing the assertions associated with the
validate-shopping-form-test unit test.
cljs.user> (require '[modern-cljs.shopping.validators-test :as v])
nil
Are you ready for the magic? Evaluate the following expression:
cljs.user> (t/run-tests 'modern-cljs.shopping.validators-test)
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
nil
Voila! The magic worked again. Kudos to everyone who put this magic together.
We are at the end of this tutorial. Stop any boot related process
and reset the repository
git reset --hard
Next step - Tutorial 15: It's better to be safe than sorry (Part 3)
In the next Tutorial of the series, we're going to do some
housekeeping with boot.
License
Copyright © Mimmo Cosenza, 2012-15. Released under the Eclipse Public License, the same as Clojure.