- •Contents
- •Foreword
- •Acknowledgments
- •Preface
- •Who This Book Is For
- •What Is in This Book
- •How to Read This Book
- •Notation Conventions
- •Web Resources and Feedback
- •Downloading Sample Code
- •Getting Started
- •Why Clojure?
- •Clojure Coding Quick Start
- •Exploring Clojure Libraries
- •Introducing Lancet
- •Wrapping Up
- •Exploring Clojure
- •Forms
- •Reader Macros
- •Functions
- •Vars, Bindings, and Namespaces
- •Flow Control
- •Metadata
- •Wrapping Up
- •Working with Java
- •Calling Java
- •Optimizing for Performance
- •Creating and Compiling Java Classes in Clojure
- •Exception Handling
- •Adding Ant Projects and Tasks to Lancet
- •Wrapping Up
- •Unifying Data with Sequences
- •Everything Is a Sequence
- •Using the Sequence Library
- •Clojure Makes Java Seq-able
- •Adding Properties to Lancet Tasks
- •Wrapping Up
- •Functional Programming
- •Functional Programming Concepts
- •How to Be Lazy
- •Lazier Than Lazy
- •Recursion Revisited
- •Wrapping Up
- •Concurrency
- •The Problem with Locks
- •Refs and Software Transactional Memory
- •Use Atoms for Uncoordinated, Synchronous Updates
- •Use Agents for Asynchronous Updates
- •Managing Per-Thread State with Vars
- •A Clojure Snake
- •Making Lancet Targets Run Only Once
- •Wrapping Up
- •Macros
- •When to Use Macros
- •Writing a Control Flow Macro
- •Making Macros Simpler
- •Taxonomy of Macros
- •Making a Lancet DSL
- •Wrapping Up
- •Multimethods
- •Living Without Multimethods
- •Moving Beyond Simple Dispatch
- •Creating Ad Hoc Taxonomies
- •When Should I Use Multimethods?
- •Adding Type Coercions to Lancet
- •Wrapping Up
- •Clojure in the Wild
- •Automating Tests
- •Data Access
- •Web Development
- •Farewell
- •Editor Support
- •Bibliography
- •Index
- •Symbols
Chapter 9
Clojure in the Wild
Now that you have learned the basics of the Clojure language, it is time for you to begin using Clojure in your own projects. But as you run out the door to start work on your killer Clojure app, you realize that you do not yet know how to use Clojure for everyday programming tasks:
•How do I write unit tests for my Clojure code?
•How do I access relational data from Clojure?
•How do I build a simple web application in Clojure?
These questions do not have a simple answer because there are so many choices. With Clojure, you have access to the entire world of Java APIs for data access, XML, GUI development, web development, testing, graphics, and more. In addition, idiomatic Clojure libraries are beginning to appear.
There are so many choices, and innovation is proceeding so swiftly, that I will not attempt to cover all the possible choices. Instead, I will simply open Pandora’s box and show you a few possible answers to the earlier questions. In this chapter, you will learn how to do the following:
•Build unit tests with the test-is library
•Access relational data with JDBC and Clojure’s sql library
•Create web applications with the Compojure framework
These examples demonstrate Clojure’s value in a wide variety of settings and should whet your appetite for the exciting world of Clojure development.
Prepared exclusively for WG Custom Motorcycles
AUTOMATING TESTS 266
9.1 Automating Tests
Clojure makes it easy to test your code in the REPL. But for any nontrivial codebase, manual testing at the REPL is not enough. You should also have a set of self-validating tests. Self-validating tests report success or failure, so you do not have to interpret results manually by reading off values at the REPL. This section introduces two testing libraries: the test function in the Clojure core and clojure.contrib.test-is.
Test with :test
Clojure attaches tests to functions with :test metadata. To see :test in action, consider the index-of-any function introduced in Section 2.6, Where’s My for Loop?, on page 70. The Java version includes the following examples in the method comment:
StringUtils.indexOfAny(null, *) |
= -1 |
StringUtils.indexOfAny("" , *) |
= -1 |
StringUtils.indexOfAny(*, null) |
= -1 |
StringUtils.indexOfAny(*, []) |
= -1 |
StringUtils.indexOfAny("zzabyycdxx" ,['z' ,'a' ]) = 0 |
|
StringUtils.indexOfAny("zzabyycdxx" ,['b' ,'y' ]) = 3 |
|
StringUtils.indexOfAny("aba" , ['z' ]) |
= -1 |
In Clojure, you can add these tests as metadata on the index-of-any function:
Download examples/index_of_any.clj
(defn index-filter [pred coll]
(when pred (for [[idx elt] (indexed coll) :when (pred elt)] idx)))
(defn
#^{:test (fn []
(assert (nil? (index-of-any #{\a} nil))) (assert (nil? (index-of-any #{\a} "" )))
(assert (nil? (index-of-any nil "foo" )))
(assert (nil? (index-of-any #{} "foo" )))
(assert (zero? (index-of-any #{\z \a} "zzabyycdxx" )))
(assert (= 3 (index-of-any #{\b \y} "zzabyycdxx" )))
(assert (nil? (index-of-any #{\z} "aba" ))))}
index-of-any [pred coll]
(first (index-filter pred coll)))
The :test metadata key takes as its value a test function. In the previous example, the test function exercises index-of-any, using (assert expr) to test that various exprs are true.
Prepared exclusively for WG Custom Motorcycles
Report erratum
this copy is (P1.0 printing, May 2009)
AUTOMATING TESTS 267
You can run the tests from the REPL using the test function:
(test a-var)
test looks up the test function in a-var’s :test metadata and runs the test. Note that test takes a var, not a symbol. Try testing index-of-any:
(test #'index-of-any)
:ok
To see what happens when a test fails, create a test with an assertion that is not true:
Download examples/exploring.clj
(defn
#^{:test (fn []
(assert (nil? (busted))))} busted [] "busted" )
(test #'busted)
java.lang.Exception: Assert failed: (nil? (busted))
:test is simple and easy to use, and it keeps your tests as close as possible to the code they test. However, most developers are accustomed to frameworks that keep tests separate from code. Clojure-contrib provides this via the test-is library.
Test with test-is
Stuart Sierra’s test-is library lets you write tests with the deftest macro:
(clojure.contrib.test-is/deftest testname & forms)
The test forms make assertions via the is macro:
(clojure.contrib.test-is/is form message?)
form is any predicate, and message is an optional error message. You could begin testing index-of-any thusly:
Download examples/test/index_of_any.clj
(ns examples.test.index-of-any
(:use examples.index-of-any clojure.contrib.test-is))
(deftest test-index-of-any-with-nil-args (is (nil? (index-of-any #{\a} nil))) (is (nil? (index-of-any nil "foo" ))))
(deftest test-index-of-any-with-empty-args (is (nil? (index-of-any #{\a} "" )))
(is (nil? (index-of-any #{} "foo" ))))
Prepared exclusively for WG Custom Motorcycles
Report erratum
this copy is (P1.0 printing, May 2009)
AUTOMATING TESTS 268
(deftest test-index-of-any-with-match
(is (zero? (index-of-any #{\z \a} "zzabyycdxx" )))
(is (= 3 (index-of-any #{\b \y} "zzabyycdxx" ))))
(deftest test-index-of-any-without-match (is (nil? (index-of-any #{\z} "aba" ))))
In the previous section’s :test example, the test function contained seven assertions. Here, those seven assertions are divided into four different tests. This is a matter of taste. Since deftest gives you the chance to create more than one test for the same function, it encourages you to group smaller sets of assertions under meaningful test names.
You can run all tests in a group of namespaces with run-tests:
(clojure.contrib.test-is/run-tests & namespaces)
If you do not specify any namespaces, run-tests will default to the current namespace. Assuming you entered the index-of-any tests at the REPL, you could run them with this:
(run-tests) Testing user
Ran 4 tests 7 assertions. 0 failures, 0 exceptions.
To see a test fail, create a test with a failing assertion:
Download examples/test/fail.clj
(deftest test-that-demonstrates-failure (is (= 5 (+ 2 2))))
Testing user
FAIL in (test-that-demonstrates-failure) (NO_SOURCE_FILE:5) expected: (= 5 (+ 2 2))
actual: (not= 5 4)
Ran 1 tests containing 1 assertions. 1 failures, 0 errors.
Unlike assert, is can also take an optional error message. When a test fails, is appends the optional error message after the generic error message:
Download examples/test/fail.clj
(deftest test-that-demonstrates-error-message (is (= 3 Math/PI) "PI is an integer!?" ))
Prepared exclusively for WG Custom Motorcycles
Report erratum
this copy is (P1.0 printing, May 2009)
AUTOMATING TESTS 269
Testing user
FAIL in (test-that-demonstrates-error-message) (NO_SOURCE_FILE:2) PI is an integer!?
expected: (= 3 Math/PI)
actual: (not= 3 3.141592653589793)
The is macro can, of course, also be used inside a :test function.
It is particularly important that tests cover less-used paths through code, such as error handling. To this end, you can use the (is (thrown? )) form to test that an exception occurred:
Download examples/test/exploring.clj
(deftest test-divide-by-zero
(is (thrown? ArithmeticException (/ 5 0))))
thrown? is an example of a custom assert expression. You can add your own assert expressions to test-is; see the source code for the assert-expr multimethod for details.
The example code for this book is tested using test-is. The tests demonstrate several features that are not covered here; see the examples/test and lancet/test directories for more examples using test-is.
Test However You Want
Clojure’s test capabilities are lightweight and easy to use. What if you want something that integrates with an existing build system and runs on your continuous integration box?
If you have an existing Java infrastructure, another answer is “Test with whatever framework you already use.” Clojure code is Java code.
All Clojure sequences implement Java collection interfaces. All Clojure functions implement Callable and Runnable. So, you can write your tests with any Java-compatible test framework: JUnit, TestNG, or even EasyB or JTestR. Good luck, and please consider open sourcing any reusable pieces you invent along the way.
Next, let’s take a look at how Clojure can access relational data.
Prepared exclusively for WG Custom Motorcycles
Report erratum
this copy is (P1.0 printing, May 2009)