Unit Tests

There are many schools of thought on how, what, and when to test. This is a very sensitive subject for many people. As such, I will simply give an overview of the basic tools available for testing and leave it up to you to decide how and when to use them.

The Test API

Clojure provides built-in support for testing via the clojure.test namespace. When a new project is created a test package will be generated along with it.

Let’s take a quick look at what this application programming interface (API) looks like and how to work with it. The simplest way to write tests is to create assertions using the is macro. The following are a few examples of how it works:

 
(is (​=​ 4 (​+​ 2 2)))
 
 
(is (​=​ 5 (​+​ 2 2)))
 
 
FAIL in (:1)
 
expected: (​=​ 5 (​+​ 2 2))
 
actual: (​not​ (​=​ 5 4))
 
false
 
 
(is (​even?​ 2))
 
 
(is (​instance?​ String 123))
 
FAIL in (:1)
 
expected: (​instance?​ String 123)
 
actual: java.lang.Long
 
false

As you can see, the is macro can take any expression. If the expression fails, the macro will print the expression along with the actual result, then return false; otherwise it will return true.

We can also group our tests together by using the testing macro. This macro accepts a string name for the group of tests followed by the assertions.

 
(testing ​"Collections"
 
(is (​coll?​ {}))
 
(is (​coll?​ #{}))
 
(is (​coll?​ []))
 
(is (​coll?​ '())))

Finally, we can define tests by using the deftest macro:

 
(deftest collections-test
 
(testing ​"Collections"
 
(is (​coll?​ {}))
 
(is (​coll?​ #{}))
 
(is (​coll?​ []))
 
(is (​coll?​ '()))))

The tests defined using deftest can be called like regular functions. You can also run all the tests in the read-evaluate-print loop (REPL) by calling run-tests. All tests in the application’s test folder can be run via Leiningen by calling lein test. The API contains a number of other helpers, as well, but I hope that the preceding examples will prove sufficient for you to get started.

Finally, it’s worth mentioning that there are a number of test frameworks for Clojure, such as Midje and Speclj.[47][48] Furthermore, test frameworks are available specifically for testing web applications. The two popular choices to explore are Peridot and Kerodon.[49][50]

These frameworks provide many features not found in the core testing API, and if your testing needs go beyond the basics we explored here, these will make excellent tools in your Clojure toolbox.

Testing the Application

Our application has two types of routes. There are routes that serve the user-interface (UI) portion of the application to be rendered by the browser, and those that expose the handlers for the UI actions. We’ll look at writing some tests for our application’s login handler.

We already have a test harness defined for our application. You can find it under the test/picture_gallery/test/ directory. The test handler is called handler.clj. If we open it up, we can see that it defines a test called test-app.

This test is currently failing because our application doesn’t respond with the result it expects when the / URI is requested. We’ll first identify the scenarios that we’d like to test:

  • No parameters are supplied during the login.

  • The parameters supplied do not match a user in the database.

  • The login is successful.

To request a route in our application we can use the following code:

 
(app (request <method> <url> <params>))

The response will be a standard Ring response, which was described in Chapter 2, Clojure Web Stack.

We’d like to call the /login URL and pass it the user ID and the password. However, we only wish to test the request handler and not the model. Our test shouldn’t depend on what users are currently populated in the database.

Unfortunately, the handle-login function in the picture-gallery.routes.auth namespace calls the get-user from the picture-gallery.models.db namespace. When we call it from our tests we’ll be querying the actual users in our database.

In some languages it’s possible to use monkey patching to get around this problem. This approach allows you to simply redefine the offending function at runtime with your own version. The downside of this approach is that the change is global and therefore might interact poorly with code that expects the original version.

Clojure provides a with-redefs macro that redefines Vars within the scope of its body. This approach gives us the ability to make runtime modifications in a safer fashion, where we know exactly what code is affected.

For our purposes, we’ll redefine the get-users function with a mock function for the scope of our tests. It’s handy that we didn’t have to plan for this when writing our application’s business logic. Let’s look at how this works in action. We’ll first define a mock function that will return a test user.

picture-gallery-style-tests/test/picture_gallery/test/handler.clj
 
(​defn​ mock-get-user [id]
 
(​if​ (​=​ id ​"foo"​)
 
{:id ​"foo"​ :pass (encrypt ​"12345"​)}))

We’ll also need to reference noir.util.crypt/encrypt for it to encrypt the password.

picture-gallery-style-tests/test/picture_gallery/test/handler.clj
 
(​ns​ picture-gallery.test.handler
 
(:require [clojure.test :refer :all]
 
[ring.mock.request :refer :all]
 
[noir.util.crypt :refer [encrypt]]
 
[picture-gallery.handler :refer :all]))

We can now redefine the picture-gallery.models.db/get-user with the mock function before running our test:

 
(with-redefs [picture-gallery.models.db/get-user mock-get-user]
 
(app (request :post ​"/login"​ {:id ​"foo"​ :pass ​"12345"​})))

When we run the preceding code in the REPL, we see that a redirect is returned along with a cookie containing our session ID:

 
{:status 302
 
:headers {​"Set-Cookie"​ (​"ring-session=0645d310-892b-43c0-a4d5-dcaa87859a67;Path=/"​)
 
"Location"​ ​"/"​}
 
:body ​""​}

Now we can test the case for when no user is found:

 
(with-redefs [picture-gallery.models.db/get-user mock-get-user]
 
(app (request :post ​"/login"​ {:id ​"bar"​ :pass ​"12345"​})))

This time no session is created and we’re simply redirected to the application’s / URL:

 
{:status 302
 
:headers {​"Set-Cookie"​ ()
 
"Location"​ ​"/"​}
 
:body ​""​}

Let’s put this all together and write the unit tests for the login portion of our application.

picture-gallery-style-tests/test/picture_gallery/test/handler.clj
 
(​ns​ picture-gallery.test.handler
 
(:require [clojure.test :refer :all]
 
[ring.mock.request :refer :all]
 
[noir.util.crypt :refer [encrypt]]
 
[picture-gallery.handler :refer :all]))
 
(​defn​ mock-get-user [id]
 
(​if​ (​=​ id ​"foo"​)
 
{:id ​"foo"​ :pass (encrypt ​"12345"​)}))
 
(deftest test-login
 
(testing ​"login success"
 
(with-redefs [picture-gallery.models.db/get-user mock-get-user]
 
(is
 
(​->​ (request :post ​"/login"​ {:id ​"foo"​ :pass ​"12345"​})
 
app :headers (​get​ ​"Set-Cookie"​) ​not-empty​))))
 
 
(testing ​"password mismatch"
 
(with-redefs [picture-gallery.models.db/get-user mock-get-user]
 
(is
 
(​->​ (request :post ​"/login"​ {:id ​"foo"​ :pass ​"123456"​})
 
app :headers (​get​ ​"Set-Cookie"​) ​empty?​))))
 
 
(testing ​"user not found"
 
(with-redefs [picture-gallery.models.db/get-user mock-get-user]
 
(is
 
(​->​ (request :post ​"/login"​ {:id ​"bar"​ :pass ​"12345"​})
 
app :headers (​get​ ​"Set-Cookie"​) ​empty?​)))))
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.72.78