In the previous tutorial we introduced the CLJ/CLJS standard way
to unit test namespaces. To reach that result we dynamically
altered the boot
runtime environment.
In this tutorial we start by freezing that needed change in the build.boot
file. Then we go on to configure a development environment
that allows a single JVM to simultaneously satisfy the Immediate Feedback
Principle by Bret Victor and enable Test Driven Development (TDD).
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-14
In the previous tutorial, we added a few assertions to unit test the
modern-cljs.shopping.validators
namespace. We then exercised those
assertions by manually calling run-tests
from the CLJ REPL and the
CLJS bREPL as well.
As you remember, before running those unit tests we had first to alter
the IFDE runtime environment by adding the test/cljc
directory to
the :source-paths
environment variable and then, depending on the
runtime platform of the active REPL, we had to require the appropriate
CLJ/CLJS namespace together with the namespace containing the unit
tests themselves.
Obviously this manual workflow is only acceptable while you're learning how to introduce unit testing in your CLJ/CLJS mixed project.
In this tutorial, our objective is to progressively reduce as much as possible the number of manual steps. We'll end up with a kind of live development environment that will satisfy Bret Victor's Immediate Feedback Principle and also enable a TDD workflow.
The first thing we want to eliminate is the need to add the
test/cljc
directory to the :source-paths
environment variable each
time we start the IFDE runtime.
Let's create a new task in the build.boot
configuration file and name it testing
:
(deftask testing
"Add test/cljc for CLJ/CLJS testing purpose"
[]
(set-env! :source-paths #(conj % "test/cljc"))
identity)
As you can see, this new task, which must be added immediately before the
dev
task, replicates the same thing we did manually in the previous
tutorial while the IFDE was running. The identity
function is
returning the task itself in such a way that you can compose it with
other tasks.
To see how the newly defined testing
task can be used along with the dev
task, start the IFDE as follows:
cd /path/to/modern-cljs
boot testing dev
...
Elapsed time: 26.385 sec
As you see, to compose the dev
task with the testing
one,
we only needed to place the testing
task before the dev
one.
This will add the test/cljc
directory to the :source/paths
environment
variable, as we desired.
Let's see if it works like expected by starting the CLJ REPL:
# from a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=>
Now verify that the :source-paths
environment variable has been altered by the testing
task:
boot.user> (pprint (get-env :source-paths))
#{"/Users/mimmo/.boot/cache/tmp/Users/mimmo/tmp/modern-cljs/41s/6c94bi"
"/Users/mimmo/.boot/cache/tmp/Users/mimmo/tmp/modern-cljs/41s/-9b70fr"
"src/cljs" "test/cljc" "src/cljc" "src/clj"}
nil
The test/cljc
has been correctly added. Now do the same thing
we did in the previous tutorial by requiring the clojure.test
and modern-cljs.shopping.validators-test
namespaces before
calling run-tests
:
boot.user=> (require '[clojure.test :as t]
'[modern-cljs.shopping.validators-test])
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}
So far, so good. Let's now repeat the same thing in the CLJS bREPL. First start the bREPL as usual:
boot.user=> (start-repl)
<< started Weasel server on ws://127.0.0.1:49916 >>
<< waiting for client to connect ... Connection is ws://localhost:49916
Writing boot_cljs_repl.cljs...
connected! >>
To quit, type: :cljs/quit
nil
cljs.user=>
NOTE 1: remember to visit the shopping URL to activate the bREPL.
Then require the appropriate namespaces
cljs.user=> (require '[cljs.test :as t :include-macros true]
'[modern-cljs.shopping.validators-test :as v])
nil
and finally evaluate the run-tests
function again
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
Not so bad, but we want more, especially when adopting a Test Driven Development (TDD) workflow. Before proceeding to the next step, stop
any boot
related process.
boot
does not have a test
pre-build task like
Leiningen
does. Fortunately, the boot
community created the boot-test task
for that job. Let's add it to our build.boot
file as usual.
(set-env!
...
:dependencies '[
...
[adzerk/boot-test "1.2.0"]
])
(require ...
'[adzerk.boot-test :refer [test]])
As usual take a moment to read the help documentation for this new task:
boot test -h
Run clojure.test tests in a pod.
The --namespaces option specifies the namespaces to test. The default is to
run tests in all namespaces found in the project.
The --exclusions option specifies the namespaces to exclude from testing.
The --filters option specifies Clojure expressions that are evaluated with %
bound to a Var in a namespace under test. All must evaluate to true for a Var
to be considered for testing by clojure.test/test-vars.
Options:
-h, --help Print this help info.
-n, --namespaces NAMESPACE Conj NAMESPACE onto the set of namespace symbols to run tests in.
-e, --exclusions NAMESPACE Conj NAMESPACE onto the set of namespace symbols to be excluded from test.
-f, --filters EXPR Conj EXPR onto the set of expressions to use to filter namespaces.
-r, --requires REQUIRES Conj REQUIRES onto extra namespaces to pre-load into the pool of test pods for speed.
As you see, there is a -n
command line option (i.e. :namespaces
when used inside build.boot
) that we could use to specify the namespace to
run tests in.
Let's try it
boot testing test -n modern-cljs.shopping.validators-test
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
It worked. All the assertions of the sole unit test we defined in
modern-cljs.shopping.validators-test
have been executed
and they all succeeded.
Don't forget that the boot-test
task is for CLJ only and it has
nothing to offer for CLJS. Later we'll add support for CLJS testing as well.
While we are making good progress, we still want to create a TDD-style workflow. Any time you modify your source code the corresponding unit tests should be executed again.
The composable nature of boot
tasks is the answer. Do you remember
when at the beginning of this series we introduced the watch
task
for triggering the CLJS recompilation anytime we modified a CLJS source
file? Here we're going to use watch
again to automatically run our
unit tests anytime a CLJ file is modified.
boot testing watch test -n modern-cljs.shopping.validators-test
Starting file watcher (CTRL-C to quit)...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 4.244 sec
Now modify one of the assertions in the
test/cljc/shopping/validators-test.cljc
to force 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") ;; forced bug
...))))
As soon as you save the file you'll receive the following failure:
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (validators_test.cljc:9)
Shopping Form Validation / Happy Path
expected: nil
actual: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
diff: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
Ran 1 tests containing 13 assertions.
1 failures, 0 errors.
clojure.lang.ExceptionInfo: Some tests failed or errored
data: {:test 1, :pass 12, :fail 1, :error 0, :type :summary}
clojure.core/ex-info core.clj: 4593
adzerk.boot-test/eval549/fn/fn/fn boot_test.clj: 73
boot.task.built-in/fn/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn built_in.clj: 230
boot.core/run-tasks core.clj: 701
boot.core/boot/fn core.clj: 711
clojure.core/binding-conveyor-fn/fn core.clj: 1916
...
Elapsed time: 0.473 sec
NOTE 2: on Mac OSX sometimes there is strange behavior producing a long waiting time before the recompilation is triggered.
As you see, the watch
task triggered the unit tests to run and
the modified assertion pertaining to the Happy Path
failed. Correct it and save the file again.
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 0.252 sec
Even though, for now, we're only dealing with CLJ files, our plan for supporting the TDD workflow seems to be working. But we are missing the CLJ REPL experience offered by IFDE.
Could we have both of them? I have nothing against TDD approach, but once you experienced a CLJ/CLJS REPL you'd like to have it available regardless of development style (TDD or otherwise).
For the moment, we'll postpone this requirement until later paragraphs. Indeed, we'd like first to replicate on the CLJS platform what we have already done for the CLJ platform.
Before proceeding with next step, stop the above boot
process.
If you want to test CLJS code, sooner or later you will end up testing the emitted JS in a headless browser. The most famous of them all is PhantomJS which is based on WebKit.
To install PhantomJS follow the instructions for your Operating
System. On any *nix OS it should be enough to download the compressed
file, decompress it and add its bin
directory to the PATH
environment
variable.
NOTE 3: I currently use phantomjs 1.9.2. The 2.0 release had some
issues on Mac OS X, however these have been resolved in the 2.1 release.
To be able to run CLJS unit tests with the same TDD workflow we
used for CLJ unit testing, you need to add to build.boot
the boot-cljs-test task specifically devoted for CLJS, which is
compatible with a plethora of JS Engines, including PhantomJS.
The procedure to add a new task is always the same: add it to the
dependencies section of the build.boot
file and require its exported
namespace by referring the needed symbols as well.
(set-env!
...
:dependencies '[
...
[crisptrutski/boot-cljs-test "0.2.1-SNAPSHOT"]
])
(require ...
'[crisptrutski.boot-cljs-test :refer [test-cljs]])
Then, the help documentation as usual:
boot test-cljs -h
Run cljs.test tests via the engine of your choice.
The --namespaces option specifies the namespaces to test. The default is to
run tests in all namespaces found in the project.
Options:
-h, --help Print this help info.
-e, --js-env VAL Set the environment to run tests within, eg. slimer, phantom, node,
or rhino to VAL.
-n, --namespaces NS Conj NS onto namespaces whose tests will be run. All tests will be run if
ommitted.
-s, --suite-ns NS Set test entry point. If this is not provided, a namespace will be
generated to NS.
-O, --optimizations LEVEL Set the optimization level to LEVEL.
-o, --out-file VAL Set output file for test script to VAL.
-c, --cljs-opts VAL Set compiler options for CLJS to VAL.
-u, --update-fs? Only if this is set does the next task's filset include
and generated or compiled cljs from the tests.
-x, --exit? Exit immediately with reporter's exit code.
At first glance, it looks like we should be interested in two options:
-e, --js-env
(i.e.:js-env
insidebuild.boot
), setting the JS engine to use for running tests-n, --namespaces
(i.e.:namespaces
insidebuild.boot
), setting the namespaces for which unit tests will be run
Knowing a little bit about how boot
fileset works, we
may be interested in the -u, --update-fs?
option as well, since
it allows us to populate the target
directory as cljs
does.
We're now ready to light the fire. Mutatis mutandis, we're going to
use the same task composition we've already adopted for the boot-test
task when dealing with CLJ unit testing:
boot testing watch test-cljs -u -e phantom -n modern-cljs.shopping.validators-test
Starting file watcher (CTRL-C to quit)...
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 15.975 sec
Not too shabby. We obtained the same results previously achieved with the
boot-test
task, which is exactly what we were expecting.
There are few things to be noted.
First, as we learned from the boot-cljs-test
help documentation, we
used phantom
as headless browser, and we also passed the
modern-cljs.shopping.validators-test
as the sole namespace to run
tests for.
Secondly, the boot-test-cljs
task internally uses the CLJS compiler
by overwriting some default values, as the :main option reported in the warning. Further more, instead of generating
the main.js
JS file as the boot-cljs
task did, it generates the
output.js
JS file.
NOTE 4 The warning is no longer printed out from the version 0.3.0 of boot-test-cljs. Nonetheless I prefer to continue to use the old version 0.2.1 since the new one is still facing some problems.
Now repeat the same experiments we did previously by modifying the
unit test assertion in
test/cljc/shopping/validators-test.cljc
to verify that boot
will
recompile any changed files and rerun the unit tests as soon as we save
a file:
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(testing "/ Happy Path"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "" "0" "0" "0") ;; forced bug
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))
...))
When you save the file, the watch
task triggers the CLJS
recompilation and reruns the sole unit test contained in the
modern-cljs.shopping.validators-test
namespace. Following is the
reported result:
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (:)
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 13 assertions.
1 failures, 0 errors.
Elapsed time: 4.779 sec
As you see the (= nil (validate-shopping-form "" "0" "0" "0"))
failed because (validate-shopping-form "" "0" "0" "0")
is now
returning {:quantity ["Quantity can't be empty." "Quantity has to be an integer number." "Quantity can't be negative."]}
instead of the expected nil
value.
Correct the induced bug, save the file and wait for the new report:
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 3.906 sec
Everything got recompiled and the previously failing assertion is now
passing. Before proceeding to the next step, stop the boot
process.
Even though we've gotten good results with CLJ and CLJS unit testing and the TDD workflow, there are still some things we'd like to improve:
- We'd like to combine the CLJ/CLJS unit testing into a single
boot
command, which means in a single JVM; - In turn, we'd like to combine the combined command with the
boot dev
command, again to use a single JVM; - We'd like to call the
boot
command with some sane defaults to reduce its length and the options we have to remember.
Let's start with the first goal.
As we have previously seen, the composable nature of boot
tasks allows
us to define a new task that is the result of the composition of other
previously defined tasks.
Let's try to define a new tdd
(Test Driven Development) task which
combines the test
and the cljs-test
tasks.
(deftask tdd
"Launch a TDD Environment"
[]
(comp
(testing)
(watch)
(test-cljs :update-fs? true :js-env :phantom :namespaces '#{modern-cljs.shopping.validators-test})
(test :namespaces '#{modern-cljs.shopping.validators-test})
(target :dir #{"target"})))
Note 5: The above task composition mimics the same composition we previously created at the command line, and appends the
test
task after thetest-cljs
task. The order of the two unit testing tasks is important. A subsequent tutorial will better explain the Task Options DSL used byboot
to offer the same options at the command line and as it does in thebuild.boot
file.
Place the new task definition in build.boot
after the
testing
task.
Now run the newly defined tdd
task:
cd /path/to/modern-cljs
boot tdd
Starting file watcher (CTRL-C to quit)...
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 18.276 sec
So far, so good. The boot tdd
command first compiled any CLJS files,
ran the CLJS unit tests and finally ran on the JVM the same unit tests
contained in the modern-cljs.shopping.valuators-test
namespace.
The results are exactly the expected ones. All the assertions succeeded on both the CLJS and CLJ platforms.
Now force again a failure for one of the assertions in the
validators_test.cljs
:
(deftest validate-shopping-form-test
(testing "Shopping Form Validation"
(testing "/ Happy Path"
(are [expected actual] (= expected actual)
nil (validate-shopping-form "" "0" "0" "0") ;; forced bug
nil (validate-shopping-form "1" "0.0" "0.0" "0.0")
nil (validate-shopping-form "100" "100.25" "8.25" "123.45")))
...))
After you save the file you'll receive the following report:
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (:)
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 13 assertions.
1 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (validators_test.cljc:9)
Shopping Form Validation / Happy Path
expected: nil
actual: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
diff: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
Ran 1 tests containing 13 assertions.
1 failures, 0 errors.
clojure.lang.ExceptionInfo: Some tests failed or errored
data: {:test 1, :pass 12, :fail 1, :error 0, :type :summary}
clojure.core/ex-info core.clj: 4593
adzerk.boot-test/eval549/fn/fn/fn boot_test.clj: 73
crisptrutski.boot-cljs-test/eval651/fn/fn/fn boot_cljs_test.clj: 109
adzerk.boot-cljs/eval268/fn/fn/fn boot_cljs.clj: 200
adzerk.boot-cljs/eval226/fn/fn/fn boot_cljs.clj: 134
crisptrutski.boot-cljs-test/eval621/fn/fn/fn boot_cljs_test.clj: 79
boot.task.built-in/fn/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn built_in.clj: 230
boot.core/run-tasks core.clj: 701
boot.core/boot/fn core.clj: 711
clojure.core/binding-conveyor-fn/fn core.clj: 1916
...
Elapsed time: 2.621 sec
It worked again as expected. Because of the code change in the
validators_test.cljs
file, the tdd
task recompiled all the CLJS
source files, ran the CLJS unit test, reporting the failure, and
finally ran the CLJ test reporting the same failure.
Now correct the bug you inserted above. You should see the following report:
Writing suite.cljs...
Writing output.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• output.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 2.033 sec
We again obtained the expected results, and we also addressed the first
item in the above nice-to-have list: a TDD environment
for both CLJ and CLJS running in the same JVM. Before proceeding with the
next step, stop the boot
process.
As we said at the beginning of this series, one of the most attractive
features of the boot
build tool, compared with leiningen
,
is its ability to run multiple tasks in the same JVM without
them interfering with each other. We have already unified the CLJ and the CLJS unit
testing tasks. Let's see if we are also able to run the
original dev
task in the same JVM.
As we saw previously, thanks to the watch
task, the test-cljs
task
triggers the CLJS recompilation anytime we modify and save a .cljs
or a .cljc
file.
The question now is the following: could we compose the
test-cljs
task with the serve
, reload
and cljs-repl
tasks in
the same way that we've already composed them in the dev
task with the
cljs
task?
Let's start by reading the test-cljs
help documentation again:
boot test-cljs -h
Run cljs.test tests via the engine of your choice.
The --namespaces option specifies the namespaces to test. The default is to
run tests in all namespaces found in the project.
Options:
-h, --help Print this help info.
-e, --js-env VAL Set the environment to run tests within, eg. slimer, phantom, node,
or rhino to VAL.
-n, --namespaces NS Conj NS onto namespaces whose tests will be run. All tests will be run if
ommitted.
-s, --suite-ns NS Set test entry point. If this is not provided, a namespace will be
generated to NS.
-O, --optimizations LEVEL Set the optimization level to LEVEL.
-o, --out-file VAL Set output file for test script to VAL.
-c, --cljs-opts VAL Set compiler options for CLJS to VAL.
-u, --update-fs? Only if this is set does the next task's filset include
and generated or compiled cljs from the tests.
-x, --exit? Exit immediately with reporter's exit code.
That's interesting. Aside form the -e
, -n
, -s
, -u
and -x
options, the remaining -O
, -o
and -c
options seem to deal with
the CLJS compiler options.
At the moment we're only interested in the -o
(i.e. :out-file
when
used inside build.boot
) option, because it is the option for setting
the name of JS file generated by the CLJS compiler. As we previously saw while
playing with the newly defined tdd
task, the default filename is
output.js
.
Recall that the default filename generated by the cljs
task that we
composed in the dev
task and included in the html
pages is
main.js
.
Let's try to rearrange the tdd
task composition by:
- prepending the
serve
task in the same way we did for thedev
task; - adding the
reload
task to trigger the reloading of static resources as we did for thedev
task. I added the :ws-host to specify the default host for web-socket; - adding the
cljs-repl
task immediately before thetest-cljs
task in the same way we did for thedev
task; - passing the
"main.js"
value to the:out-file
option for thetest-cljs
task.
Here is the updated tdd
task definition. Please substitute it for the
previous one in build.boot
:
(deftask tdd
[]
(comp
(serve :handler 'modern-cljs.core/app
:resource-root "target"
:reload true)
(testing)
(watch)
(reload :ws-host "localhost")
(cljs-repl)
(test-cljs :out-file "main.js"
:js-env :phantom
:namespaces '#{modern-cljs.shopping.validators-test}
:update-fs? true)
(test :namespaces '#{modern-cljs.shopping.validators-test})
(target :dir #{"target"})))
OK, we're ready. Let's see if we were able to build a development environment that simultaneously satisfies Bret Victor's Immediate Feedback Principle and enables the TDD workflow for both the client and the server code. And, all of that should run in a single JVM.
cd /path/to/modern-cljs
boot tdd
Starting reload server on ws://localhost:51346
Writing boot_reload.cljs...
Writing boot_cljs_repl.cljs...
2015-12-10 19:19:57.361:INFO::clojure-agent-send-off-pool-0: Logging initialized @14127ms
2015-12-10 19:20:02.689:INFO:oejs.Server:clojure-agent-send-off-pool-0: jetty-9.2.10.v20150310
2015-12-10 19:20:02.753:INFO:oejs.ServerConnector:clojure-agent-send-off-pool-0: Started ServerConnector@4079c6c9{HTTP/1.1}{0.0.0.0:3000}
2015-12-10 19:20:02.754:INFO:oejs.Server:clojure-agent-send-off-pool-0: Started @19520ms
Started Jetty on http://localhost:3000
Starting file watcher (CTRL-C to quit)...
nREPL server started on port 51347 on host 127.0.0.1 - nrepl://127.0.0.1:51347
Writing suite.cljs...
Writing main.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• main.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 29.060 sec
So far, so good. The web server started and the unit tests have been executed with success on both the client (i.e. CLJS) and the server sides (i.e. CLJ).
Now visit the usual Shopping Form and play with it.
NOTE 6: Remember that even if we already defined and tested the validators for both the client and the server sides of the Shopping Calculator, we still have to attach them to the form itself. So, if you do not follow the happy path you'll get an error anyway.
Everything is still working as expected. Now run the CLJ REPL as usual:
# from a new terminal
boot repl -c
...
boot.user>
And play with it
boot.user> (require '[clojure.test :as t]
'[modern-cljs.shopping.validators-test])
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}
Still working.
NOTE 7: I use emacs cider (release 0.10.0). It means that I can create more
nrepl-client
connections with the runningnrepl-server
implicitly started from the aboveboot tdd
command. I use one nrepl connection for the CLJ REPL, and second nrepl connection for the CLJS bREPL without starting any new JVM instances. In other words I run everything on a single JVM instance. However, when you run the aboveboot repl -c
command, you're creating a new JVM instance and if you want to start a CLJS bREPL while keeping the CLJ REPL in foreground, you'll end up with a total of three JVM instance, without counting the one eventually created by your Editor/IDE.
If you are interested in learning about Emacs and CIDER [here are some resources] (https://github.com/magomimmo/modern-cljs/blob/master/doc/supplemental-material/emacs-cider-references.md).
Now start the CLJS bREPL
boot.user> (start-repl)
<< started Weasel server on ws://127.0.0.1:51363 >>
<< waiting for client to connect ... Connection is ws://localhost:51363
Writing boot_cljs_repl.cljs...
connected! >>
To quit, type: :cljs/quit
nil
And play with it
cljs.user> (require '[cljs.test :as t :include-macros true]
'[modern-cljs.shopping.validators-test])
nil
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.
WARNING: doo's exit function was not properly set
#object[TypeError TypeError: Cannot read property 'call' of null]
nil
Still working, even if we're receiving a warning that regards
doo
, a library internally used by
test-cljs
to run cljs.test
on different JS Environment. As a final
confirmation, repeat the kind of forced failures we did before:
Writing suite.cljs...
Writing main.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• main.js
Running cljs tests...
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (:)
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 13 assertions.
1 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
FAIL in (validate-shopping-form-test) (validators_test.cljc:9)
Shopping Form Validation / Happy Path
expected: nil
actual: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
diff: {:quantity
["Quantity can't be empty."
"Quantity has to be an integer number."
"Quantity can't be negative."]}
Ran 1 tests containing 13 assertions.
1 failures, 0 errors.
clojure.lang.ExceptionInfo: Some tests failed or errored
data: {:test 1, :pass 12, :fail 1, :error 0, :type :summary}
clojure.core/ex-info core.clj: 4593
adzerk.boot-test/eval549/fn/fn/fn boot_test.clj: 73
crisptrutski.boot-cljs-test/eval651/fn/fn/fn boot_cljs_test.clj: 109
adzerk.boot-cljs/eval268/fn/fn/fn boot_cljs.clj: 200
adzerk.boot-cljs/eval226/fn/fn/fn boot_cljs.clj: 134
crisptrutski.boot-cljs-test/eval621/fn/fn/fn boot_cljs_test.clj: 79
adzerk.boot-cljs-repl/eval491/fn/fn/fn boot_cljs_repl.clj: 171
boot.task.built-in/fn/fn/fn/fn built_in.clj: 284
boot.task.built-in/fn/fn/fn/fn built_in.clj: 281
adzerk.boot-reload/eval391/fn/fn/fn/fn boot_reload.clj: 120
adzerk.boot-reload/eval391/fn/fn/fn boot_reload.clj: 119
boot.task.built-in/fn/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn/fn built_in.clj: 233
boot.task.built-in/fn/fn/fn/fn built_in.clj: 230
pandeiro.boot-http/eval314/fn/fn/fn boot_http.clj: 83
boot.core/run-tasks core.clj: 701
boot.core/boot/fn core.clj: 711
clojure.core/binding-conveyor-fn/fn core.clj: 1916
...
Elapsed time: 6.369 sec
You'll get the same results as in the previous forced failure. Now correct the forced bug and observe the report again:
Writing suite.cljs...
Writing main.cljs.edn...
Compiling ClojureScript...
WARNING: Replacing ClojureScript compiler option :main with automatically set value.
• main.js
Running cljs tests...Unexpected response code: 400
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Testing modern-cljs.shopping.validators-test
Ran 1 tests containing 13 assertions.
0 failures, 0 errors.
Elapsed time: 3.418 sec
Even though we've succeeded again, there are still a few things we'd like to improve.
The boot tdd
command does not accept any command-line options, so we have to
accept the ones hardwired into the task definition in build.boot
.
What if we want to compile CLJS with a different optimization level
from the default none
value? What if we create new namespaces for
testing other portions of the source code? And what if we want to
specifically test non portable CLJ or CLJS code?
For now, stop any boot
process and reset the git repository
git reset --hard
In the next tutorial we're going to make the tdd
task more
customizable in order to please TDD practitioners.
Copyright © Mimmo Cosenza, 2012-2016. Released under the Eclipse Public License, the same as Clojure.