This document outlines the development workflow when using Clornichon for Behavior-Driven Development (BDD) in Clojure projects. It covers the complete journey from writing scenarios to executing and maintaining them.
The Clornichon development workflow follows these main steps:
.feature files)deffeatureLet's explore each step in detail.
Feature files use the Gherkin syntax and typically have a .feature extension. They describe behaviors from a user's perspective.
Feature: Shopping Cart
As a customer
I want to manage items in my cart
So that I can purchase what I need
Scenario: Add item to empty cart
Given I have an empty shopping cart
When I add "Clojure Programming" book to the cart
Then my cart should contain 1 item
And the item should be "Clojure Programming" book
Scenario: Calculate discounts
Given the following products in catalog:
| product | price | category |
| Keyboard | 100 | hardware |
| Mouse | 50 | hardware |
| Clojure | 40 | book |
When I apply the "SUMMER10" discount code
Then the prices should be:
| product | discounted_price |
| Keyboard | 90 |
| Mouse | 45 |
| Clojure | 36 |
Scenario Outline: Apply tax based on location
Given a product with price <base_price>
When shipping to <location>
Then the final price should be <final_price>
Examples:
| base_price | location | final_price |
| 100 | US | 108 |
| 100 | EU | 120 |
| 100 | AU | 110 |
Doc strings (delimited by triple quotes) are useful for passing multi-line text content:
Scenario: Create a blog post with markdown
Given I am logged in as an author
When I create a new blog post with content:
"""
# Introduction to Clojure
Clojure is a dynamic, general-purpose programming language.
## Key Features
- Functional programming
- Immutable data structures
- Runs on the JVM
"""
Then the post should be formatted as HTML
And the title should be "Introduction to Clojure"
Doc strings are commonly used for:
After writing the feature file, you need to reference it in your Clojure code using deffeature.
(ns my-project.shopping-cart-test
(:require [clojure.test :refer :all]
[scenari.v2.core :as scenari :refer [deffeature]]))
;; Reference to the feature file
(deffeature shopping-cart "resources/features/shopping_cart.feature")
This creates a test that can be executed by Clojure's test runner. The deffeature macro:
You can customize the feature execution with options:
(deffeature shopping-cart "resources/features/shopping_cart.feature"
{:pre-run [#'setup-database]
:post-run [#'teardown-database]
:pre-scenario-run [#'setup-cart]
:post-scenario-run [#'cleanup-cart]
:default-scenario-state {:user-id "test-user"}})
Step definitions (also called "glue code") connect the Gherkin steps with actual Clojure code. Clornichon provides macros for defining these connections.
(ns my-project.shopping-cart-test
(:require [clojure.test :refer :all]
[scenari.v2.core :as scenari :refer [deffeature defgiven defwhen defthen]]))
(defgiven "I have an empty shopping cart" [state]
(assoc state :cart []))
(defwhen "I add {string} book to the cart" [state book-title]
(update state :cart conj {:title book-title :type :book}))
(defthen "my cart should contain {number} item" [state item-count]
(is (= item-count (count (:cart state)))))
(defthen "the item should be {string} book" [state book-title]
(is (= book-title (-> state :cart first :title))))
Clornichon supports various parameter types in step definitions:
A sentence matcher is a cucumber expression, so its tokens are cucumber's own:
{string}: a quoted string — single or double quotes — passed without its quotes{int}, {float}, {word}, and the other built-in types{number}: clornichon's own, kept for the glues written before the others existed; it accepts a sign and decimalsapple(s) and alternation hot/cold; a literal ( or / must be escaped (\/)A glue defined with a #"..." literal stays a plain regex whatever it contains — reading it as an expression would turn a ([^"]*) into optional text and raise on its \" — and it matches the whole sentence, as re-matches did. Its capture groups become the arguments, so a group that only groups must be made non-capturing ((?:a|b)). A string sentence wrapped in ^...$ or /.../ is read as a regex too.
(defgiven "the following products in catalog:" [state table-data]
(assoc state :products
(into {} (map (fn [row] [(:product row) row]) table-data))))
(defwhen "I create a new blog post with content:" [state doc-string]
;; doc-string contains the multi-line text from the feature file
(let [parsed-post (parse-markdown doc-string)]
(assoc state :post {:content doc-string
:title (extract-title parsed-post)
:html (markdown->html doc-string)})))
Each step function receives the state from the previous step, and what it returns is the state of the next one. This allows for data to flow through your scenario.
A step whose last form returns nil, true or false -- an assertion, a doseq, a println -- keeps the state it was given: a defthen that only asserts needs no trailing state, as above. See State and hooks.
When a feature is executed, Clornichon performs the following steps:
The step matching process is a key part of Clornichon:
A deffeature is a deftest: clojure.test/run-tests, your editor's test runner or any clojure.test runner picks it up.
(clojure.test/run-tests 'my-project.shopping-cart-test)
Clornichon integrates with Kaocha for more advanced test execution, once a suite of type :kaocha.type/scenari is declared in tests.edn -- see Running features:
clojure -M:test -m kaocha.runner # Run all tests
clojure -M:test -m kaocha.runner --focus my-test # Run specific test
The test output will show each scenario and step execution:
________________________
@checkout
Feature : Shopping Cart
A cart holds items and can be emptied.
Testing scenario : Add item to empty cart
Given I have an empty shopping cart (from my-project.glue/"I have an empty shopping cart")
When I add "Clojure Programming" book to the cart (from my-project.glue/"I add {string} book to the cart")
Then my cart should contain 1 item (from my-project.glue/"my cart should contain {number} item")
And the item should be "Clojure Programming" book (from my-project.glue/"the item should be {string} book")
________________________
The output is colorized along the gherkin syntax - bold cyan keywords, yellow
{string}/{number} parameters, cyan tags, grey descriptions, docstrings and
datatables - and the sentence takes the colour of the step's outcome: green when it
passed, red when it failed, grey when it never ran. Pass --no-color (or run under
kaocha with colour disabled) for plain output.
When a step fails, Clornichon provides information about the failure:
Then my cart should contain 1 item (from my-project.glue/"my cart should contain {number} item")
Step failed
FAIL in () (glue.clj:42)
expected: (= 1 (count (:items state)))
actual: (not (= 1 0))
And the item should be "Clojure Programming" book (from my-project.glue/"the item should be {string} book")
Add item to empty cart FAILED
The failing step is printed in red, and every step after it in grey - they are reported as pending rather than dropped, so the scenario stays readable end to end.
To examine the state passed between steps, run the feature as data: scenari.v2.core/run-feature returns each step with its :input-state and its :output-state. See Running features.
When multiple step definitions match a step, Clornichon uses namespace proximity to choose:
Define your own {token} with define-parameter-type!: a regex and the function that converts its capture. See Step expressions.
Clornichon supports several hook points for setup and teardown:
A hook can receive the scenario's name, tags and status, and be restricted to some tags: see State and hooks.
These are declared in the options of each deffeature. A hook every feature needs is declared once, as a global hook: a var marked :scenari/hook, which also gives :before-all and :after-all around the whole suite. See Global hooks.
For the maintainers of the library. release.sh tags, builds, publishes to Clojars and pushes. A release takes three steps.
The pom points to the tag, and cljdoc builds its documentation from it: the docs have to be ready in the commit the tag lands on. In one commit, Prepare 0.1.12:
[Unreleased] section of CHANGELOG.md as the version to come, # [0.1.12] - 2026-09-28;README.md and in doc/getting-started.md.Without the Clojars credentials, the script checks that and stops before it tags:
./release.sh patch
# Docs are ready for 0.1.12
# ... CLOJARS_USERNAME is not set
It says what is missing, and refuses a working tree with uncommitted changes:
CHANGELOG.md has no dated section for 0.1.12
README.md does not install 0.1.12
doc/getting-started.md does not install 0.1.12
Nothing tagged: commit the "Prepare 0.1.12" first.
CLOJARS_USERNAME=... CLOJARS_PASSWORD=<deploy token> ./release.sh major|minor|patch
The script bumps the version of the nearest tag, writes it into src/scenari/meta.clj, commits and tags. It then builds the jar, deploys it to Clojars, and pushes the commit and the tag only once Clojars accepted the artifact. If the tag is not the one the docs were prepared for, nothing is published: the tag and its commit stay local.
./build.sh alone builds the jar of the current version and installs it in the local Maven repository: under ~/.m2 it replaces the artifact of that version downloaded from Clojars.
The Clornichon development workflow provides a structured approach to Behavior-Driven Development in Clojure. By following the pattern of writing features, defining glue code, and executing tests, you can create living documentation that verifies your application's behavior.
Remember that the true value of BDD comes from the collaborative process—use feature files as a communication tool between developers, testers, and domain experts to ensure a shared understanding of requirements and behaviors.
Can you improve this documentation? These fine people already did:
Hiram MADELAINE & davidpanzaEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |