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.
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
Before introducing code testing we first want to accomplish a few other things:
- review the Shopping Form
- break the Shopping Form
- add field validations
As usual we first start the IFDE live environment
cd /path/to/modern-cljs
boot dev
...
Elapsed time: 23.261 sec
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.
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.
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.
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.AssertionError
because there is nons
form 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
quantity
has to be a positve integer - the values of
price
,tax
anddiscount
have 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.
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.
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.
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
ns
form. This is becauseboot
still does not know anything about thetest
directory structure in which the newly created file lives.
You'll end up the following test structure
test
└── cljc
└── modern_cljs
└── shopping
└── validators_test.cljc
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.
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-test
namespace compatible with both CLJ and CLJS by using the#?
reader literal. Depending on the available feature at compile-time,:clj
of:cljs
, the reader will cause the correct namespace to be required in thens
declaration.
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).
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
is
withare
in the:refer
/:refer-macros
sections 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.
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
testing
symbol in the:refer
/:refer-macros
sections 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")))))
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).
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.
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.
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}
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.
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>
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
In the next Tutorial of the series, we're going to do some
housekeeping with boot
.
Copyright © Mimmo Cosenza, 2012-15. Released under the Eclipse Public License, the same as Clojure.