Testing Clojure web applications with Kerodon

Kerodon aims to provide a friendly interface for interacting with Clojure web applications in test code. Read how to set it up on an Compojure application.

Brought to you by

Semaphore

Clojure standard library includes a general unit testing library clojure.test. There are several other libraries created for a specific purpose in mind. One of them is Kerodon, inspired by Ruby's Capybara. Kerodon helps you test Ring-based web applications by simulating how a user would interact with your app.

The goal of Kerodon is to provide a friendly interface for interacting with web applications in test code. For example, a test written in Kerodon looks like this:

(deftest user-sign-up
  (-> (session app)
    (visit "/")
    (follow "Sign up")
    (fill-in "Email:" "john@example.com")
    (fill-in "Password:" "password123")
    (press "Sign up")
    (follow-redirect)
    (has (text? "Welcome"))))

In this tutorial you will learn how to:

  • Create a Hello World Compojure application project.
  • Add Kerodon to the project.
  • Write a test for the application homepage.
  • Interact with a web application in tests.

Prerequisites

For developing Clojure application from the tutorial you need:

Create a Hello World Compojure application

Compojure is a routing library for Ring - and a popular choice for writing web applications in Clojure. Leiningen provides a Compojure template that allows us to get started with Compojure quickly.

Create Compojure based Clojure project:

lein new compojure kerodon-tutorial

The second parameter compojure is the name of the template that's going to be used for creating the application. The last parameter kerodon-tutorial is the name of your project.

Navigate to the project directory:

cd kerodon-tutorial

Start the server:

lein ring server-headless

After the server starts, visit http://localhost:3000 in a browser and you should see Hello World greeting from the application:

Hello World

Compojure application structure

The structure of your application should look like this:

β”œβ”€β”€ project.clj
β”œβ”€β”€ README.md
β”œβ”€β”€ resources
β”‚Β Β  └── public
β”œβ”€β”€ src
β”‚Β Β  └── kerodon_tutorial
β”‚Β Β      └── core
β”‚Β Β          └── handler.clj
β”œβ”€β”€ target
β”‚Β Β  β”œβ”€β”€ ...
└── test
    └── kerodon_tutorial
        └── core
            └── handler_test.clj

The file that we're interested in is src/kerodon_tutorial/core/handler.clj. If you open it, it should contain the following code:

(ns kerodon-tutorial.core.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

It defines the access point to the application (/ - the root path) and we can see that is where that "Hello World" is coming from.

We can also notice that Leiningen created the handler_test.clj file that's using clojure.test to test the handler. Since we're concentrating on Kerodon instead, let's remove the test:

rm test/kerodon_tutorial/core/handler_test.clj

First Kerodon test

Install Kerodon by adding the project dependency to project.clj:

(defproject kerodon-tutorial "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :min-lein-version "2.0.0"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [compojure "1.3.1"]
                 [ring/ring-defaults "0.1.2"]]
  :plugins [[lein-ring "0.8.13"]]
  :ring {:handler kerodon-tutorial.core.handler/app}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [kerodon "0.5.0"]
                        [ring-mock "0.1.5"]]}})

Create the features directory where you will put Kerodon tests:

mkdir test/kerodon_tutorial/features

Write the first Kerodon test in test/kerodon_tutorial/features/homepage.clj:

(ns kerodon-tutorial.features.homepage
  (:require [kerodon-tutorial.core.handler :refer [app]]
            [kerodon.core :refer :all]
            [kerodon.test :refer :all]
            [clojure.test :refer :all]))

(deftest homepage-greeting
  (-> (session app)
    (visit "/")
    (has (text? "Welcome to Kerodon Tutorial"))))

Run the tests suite:

lein test

You will see that the test failed because the greeting message does not match what we specified:

FAIL in (homepage-greeting) (homepage.clj:10)
expected: (text? "Welcome to Kerodon Tutorial")
  actual: "Hello World"

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.

Update the src/kerodon_tutorial/core/handler.clj to return the new greeting on the homepage:

(ns kerodon-tutorial.core.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Welcome to Kerodon Tutorial")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

Run the tests again:

lein test

And you will see that the test passed:

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.

Congratulations! You now have a working Compojure based application ready to be tested with Kerodon.

Interacting with web application in tests

Let's write a more complex test that will interact with the web application through a form. Our goal is to develop a feature that will allow the web site visitor to enter his name in a form, submit the form and get a nice personalized greeting.

This is the screen which we are building:

Homepage Form

And after submitting the form:

Personalized Greeting

Let's write a test that visits the home page, submits the form and checks if there is a greeting on the page. Open test/kerodon_tutorial/features/greeting.clj and add the new test:

(ns kerodon-tutorial.features.greeting
  (:require [kerodon-tutorial.core.handler :refer [app]]
            [kerodon.core :refer :all]
            [kerodon.test :refer :all]
            [clojure.test :refer :all]))

(deftest personalized-greeting
  (-> (session app)
      (visit "/")
      (fill-in "Enter your name: " "John")
      (press "Greet me!")
      (within [:h1]
        (has (text? "Hello John")))))

In the test, we can see how fill-in and press can be used to interact with a form:

  • fill-in receives the label of the field. Instead of the label, it can also find the field by a CSS selector, for example (fill-in :#name "John").
  • press receives the text value of the button, but can also find the button by a CSS selector.

When you run the tests with lein test, you will see a failure field could not be found with selector "Enter your name: ".

Let's add the form to the homepage. For that, we will use Hiccup library. Hiccup provides a DSL for rendering HTML. For example, to render a simple span you would use (html [:span {:class "foo"} "bar"]) that renders "<span class=\"foo\">bar</span>".

Open the project.clj file and add Hiccup as dependency:

(defproject kerodon-tutorial "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :min-lein-version "2.0.0"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [compojure "1.3.1"]
                 [hiccup "1.0.5"]
                 [ring/ring-defaults "0.1.2"]]
  :plugins [[lein-ring "0.8.13"]]
  :ring {:handler kerodon-tutorial.core.handler/app}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [kerodon "0.5.0"]
                        [ring-mock "0.1.5"]]}})

Now, add the form to the homepage using Hiccup:

(ns kerodon-tutorial.core.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [hiccup.core :refer [html]]
            [ring.util.anti-forgery :refer [anti-forgery-field]]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defn homepage []
  (html [:h1 "Welcome to Kerodon Tutorial"]
        [:form {:action "/greeting" :method "post"}
         (anti-forgery-field)
         [:label {:for "name"} "Enter your name: "]
         [:input {:type "text" :id "name" :name "name"}]
         [:input {:type "submit" :value "Greet me!"}]]))

(defroutes app-routes
  (GET "/" [] (homepage))
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

You can see that instead of returning a string, the / route now calls a function that returns the HTML with the form.

Inside the form generating code, you can see (anti-forgery-field). The function generates a token that prevents cross-site request forgery. If you leave out the token, you won't be able to submit the form successfully as the request will be flagged as suspicious since it doesn't have the token.

If you run the server with lein ring server-headless and visit http://localhost:3000 in the browser, you should see the form on the homepage.

Let's run the tests again to check if the new test is passing:

lein test

We can see that the new test is still failing, but we also broke the existing homepage test since the text on the homepage changed:

FAIL in (personalized-greeting) (greeting.clj:13)
expected: (text? "Hello John")
  actual: ""

FAIL in (homepage-greeting) (homepage.clj:10)
expected: (text? "Welcome to Kerodon Tutorial")
  actual: "Welcome to Kerodon TutorialEnter your name: "

Ran 2 tests containing 2 assertions.
2 failures, 0 errors.
Tests failed.

Let's update the homepage-greeting test in test/kerodon_tutorial/features/homepage.clj, before we continue further:

(deftest homepage-greeting
  (-> (session app)
      (visit "/")
      (within [:h1]
        (has (text? "Welcome to Kerodon Tutorial")))))

Here, we can see how within is used to scope the test to the HTML node we care about. It receives a CSS selector that selects the node. Note that you can use a more complex CSS selector, for example (within [:.content div.avatar a.name] ...).

If you run the tests again, you will see that the homepage is green.

The final step in developing the feature is to generate the greeting based on user's input. Open src/kerodon_tutorial/core/handler.clj and add the new code:

(ns kerodon-tutorial.core.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [hiccup.core :refer [html]]
            [ring.util.anti-forgery :refer [anti-forgery-field]]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defn homepage []
  (html [:h1 "Welcome to Kerodon Tutorial"]
        [:form {:action "/greeting" :method "post"}
         (anti-forgery-field)
         [:label {:for "name"} "Enter your name: "]
         [:input {:type "text" :id "name" :name "name"}]
         [:input {:type "submit" :value "Greet me!"}]]))

(defn greeting [name]
  (html [:h1 (str "Hello " name)]))

(defroutes app-routes
  (GET "/" [] (homepage))
  (POST "/greeting" [name] (greeting name))
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

We added 2 new things here:

  • We have the new POST /greeting route that receives the name parameter and calls greeting function passing the parameter.
  • And we have the greeting function that generates the greeting using the user's input.

If you run the tests again you will see that they pass:

Ran 2 tests containing 2 assertions.
0 failures, 0 errors.

Wrapping up

Kerodon provides an intuitive API for interacting with web pages and writing acceptance tests. It's worth noting that Kerodon doesn't execute JavaScript. If you need to write a test that tests JavaScript backed functionality, you should use something like clj-webdriver and Selenium.

In the tutorial we showed how to:

  • generate a new Compojure project using Leiningen
  • add Kerodon to the project
  • write 2 simple feature tests that interact with the web page and submit the form
  • use Hiccup to generate HTML

Although the tests we wrote are simple and short, they already show the most important aspects of Kerodon. To learn more about the Kerodon API visit the Kerodon readme.

81dc6254e4ae1e5578bfba393a844bd4
NebojΕ‘a StričeviΔ‡

An independent Ruby on Rails and JavaScript consultant. Visit my website for more info.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.