An idiomatic Clojure wrapper around cucumber-jvm, for writing Cucumber feature tests.
Library name inspired by Roman Ostash.
Add it to your deps.edn
:
{:deps {net.clojars.danielmiladinov/burpless {:mvn/version "1.0.0"}}}
Add it to your project.clj
:
[net.clojars.danielmiladinov/burpless "1.0.0"]
Save the following as test/my-first.feature
:
Feature: My first feature
Scenario: Learning to use Burpless
Given I have a string value of "Hello, Burpless!" under the :message key in my state
And I have a long value of 5 under the :stars key in my state
And I have a table of the following high and low temperatures:
| 81 | 49 |
| 88 | 54 |
| 76 | 56 |
| 70 | 48 |
| 81 | 55 |
When I am ready to check my state
Then my state should be equal to the following Clojure literal:
"""edn
{:message "Hello, Burpless!"
:stars 5
:highs-and-lows [[81 49] [88 54] [76 56] [70 48] [81 55]]
:ready-to-check? true}
"""
Yes, burpless supports DataTable
and DocString
step arguments! More on that later.
Save the following as test/my-first-feature-test.clj
.
For now, it's relatively empty, but we'll be adding more to it shortly:
(ns my-first-feature-test
(:require [clojure.test :refer [deftest is]]
[burpless :refer [run-cucumber step]]))
(def steps
[])
(deftest my-first-feature
(is (zero? (run-cucumber "test/my-first.feature" steps))))
Run the test using your preferred test runner. Below is just one possible method:
$ clojure -T:build test
You should see output similar to the following:
Running tests in #{"test"}
Testing my-first-feature-test
Scenario: Learning to use Burpless # test/my-first.feature:3
Given I have a string value of "Hello, Burpless!" under the :message key in my state
And I have a long value of 5 under the :stars key in my state
And I have a table of the following high and low temperatures:
| 81 | 49 |
| 88 | 54 |
| 76 | 56 |
| 70 | 48 |
| 81 | 55 |
When I am ready to check my state
Then my state should be equal to the following Clojure literal:
"""edn
{:message "Hello, Burpless!"
:stars 5
:highs-and-lows [[81 49] [88 54] [76 56] [70 48] [81 55]]
:ready-to-check? true}
"""
Undefined scenarios:
file:///path/to/my-first.feature:3 # Learning to use Burpless
1 Scenarios (1 undefined)
5 Steps (4 skipped, 1 undefined)
0m0.062s
You can implement missing steps with the snippets below:
(step :Given "I have a string value of {string} under the {keyword} key in my state"
(fn [state ^String string ^clojure.lang.Keyword keyword]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Given "I have a long value of {int} under the {keyword} key in my state"
(fn [state ^Integer int1 ^clojure.lang.Keyword keyword]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Given "I have a table of the following high and low temperatures:"
(fn [state ^io.cucumber.datatable.DataTable dataTable]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :When "I am ready to check my state"
(fn [state]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Then "my state should be equal to the following Clojure literal:"
(fn [state ^clojure.lang.IObj fromDocString]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
FAIL in (my-first-feature) (my_first_feature_test.clj:9)
expected: (zero? (run-cucumber "test/my-first.feature" steps))
actual: (not (zero? 1))
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Execution error (ExceptionInfo) at build/test (build.clj:19).
Tests failed
Full report at:
/path/to/full-report.edn
While these step functions in their current form definitely will not make the feature pass, they will at least give us a good starting-off point to build towards a possible solution.
(step :Given "I have a string value of {string} under the {keyword} key in my state"
(fn [state ^String string ^clojure.lang.Keyword keyword]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Given "I have a long value of {int} under the {keyword} key in my state"
(fn [state ^Integer int1 ^clojure.lang.Keyword keyword]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Given "I have a table of the following high and low temperatures:"
(fn [state ^io.cucumber.datatable.DataTable dataTable]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :When "I am ready to check my state"
(fn [state]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
(step :Then "my state should be equal to the following Clojure literal:"
(fn [state ^clojure.lang.IObj fromDocString]
;; Write code here that turns the phrase above into concrete actions
(throw (io.cucumber.java.PendingException.))))
If you are following closely, you'll see some things that may not immediately make sense:
- Why does each step function take a first argument called
state
? - Why are there type hints on all of the other step function argumnents? Are they necessary? Let's try to answer these questions in the next section.
For every call to run-cucumber
, Burpless instantiates a Cucumber Backend as well as a Cucumber runtime, with which
to test your feature. To keep things simple, it expects to run only a single feature at a time. For your convenience,
run-cucumber
also maintains an atom
to hold all of the state against which
your step functions will be executed.
We use the burpless macro, step
, to define our step functions. It takes three parameters:
- A Clojure keyword representing one of the Gherkin keywords for steps, e.g. Given, When, Then, And, But
- A string representing either a CucumberExpression (preferred) or
RegularExpression pattern to match for the step
- (all regular expression patterns must start with
^
and end with$
or they will be interpreted as cucumber expressions by the Cucumber runtime.)
- (all regular expression patterns must start with
- The function to call when executing the step. Every step function will receive the current value of the state atom
as its first argument. Any output parameters (
CucumberExpression
) or capture groups (RegularExpression
) matched in the pattern, zero or more, are provided as additional arguments to the function. Finally, a step may also contain a Step Argument. The step argument can either be a Doc String or a Data Table. Step arguments are great for when you need to provide more data than can comfortably fit into a single line of the feature file.
Each step function takes as its first argument the current value of the contents of the state atom. All the additional arguments, if any, come from the arguments parsed from the feature file. It is your responsibility as the step function author to return the new / next value of the state from each step function.
You are free to write your feature tests any way you like, but a typical approach is to follow the “Arrange, Act, Assert” pattern.
- Arrange: Some of your step functions will build up a certain value in state, or maybe perform certain external initializations as side-effects.
- Act: Using the current value of state, prepare your input data needed to pass into and / or call the system under test. Typically you would observe the return value and add that to the state as well before returning.
- Assert: Based on whatever rules in your system about the relationship between inputs into and outputs coming from the system under test, make assertions about the expected output value, compared to the actual output value.
Cucumber-JVM provides snippets for you, for every step in the feature file that wasn't matched to one of your step functions. They are not generated by burpless but come from the underlying java code itself. Burpless receives the data about unmatched step, including, among other things:
- the keyword
- the step expression string
- the suggested method / function name to use
- the map of arguments that the function should accept (argument name -> argument type)
Burpless is responsible for taking these inputs and formatting them into a snippet you can use to quickly get started implementing your step functions. Since burpless receives the arguments map, we know what type they will be when Cucumber-JVM calls your step functions. For your convenience, the step function snippets contain type hints derived from the step argument information provided by Cucumber-JVM. Of course, they are optional and you a free to remove them, if you so choose. Just be aware that removing the type hints might negatively impact test execution performance somewhat.
While you might be able to come up with something slightly different, here's one possible implementation for the step functions that makes the test pass:
(ns my-first-feature-test
(:require [clojure.test :refer [deftest is]]
[burpless :refer [run-cucumber step]])
(:import (clojure.lang IObj Keyword)
(io.cucumber.datatable DataTable)
(java.lang.reflect Type)))
(def steps
[(step :Given "I have a string value of {string} under the {keyword} key in my state"
(fn [state ^String string ^Keyword kw]
(assoc state kw string)))
(step :Given "I have a long value of {long} under the {keyword} key in my state"
(fn [state ^Long long-value ^Keyword kw]
(assoc state kw long-value)))
(step :Given "I have a table of the following high and low temperatures:"
(fn [state ^DataTable data-table]
(assoc state :highs-and-lows (.asLists data-table ^Type Long))))
(step :When "I am ready to check my state"
(fn [state]
(assoc state :ready-to-check? true)))
(step :Then "my state should be equal to the following Clojure literal:"
(fn [actual-state ^IObj expected-state]
(assert (= expected-state actual-state) (str "Expected State: " expected-state "; "
"Actual State: " actual-state))))])
(deftest my-first-feature
(is (zero? (run-cucumber "test/my-first.feature" steps))))
Run the tests again:
$ clojure -T:build test
Running tests in #{"test"}
Testing my-first-feature-test
Scenario: Learning to use Burpless # test/my-first.feature:3
Given I have a string value of "Hello, Burpless!" under the :message key in my state # my_first_feature_test.clj:9
And I have a long value of 5 under the :stars key in my state # my_first_feature_test.clj:13
And I have a table of the following high and low temperatures: # my_first_feature_test.clj:17
| 81 | 49 |
| 88 | 54 |
| 76 | 56 |
| 70 | 48 |
| 81 | 55 |
When I am ready to check my state # my_first_feature_test.clj:21
Then my state should be equal to the following Clojure literal: # my_first_feature_test.clj:25
"""edn
{:message "Hello, Burpless!"
:stars 5
:highs-and-lows [[81 49] [88 54] [76 56] [70 48] [81 55]]
:ready-to-check? true}
"""
1 Scenarios (1 passed)
5 Steps (5 passed)
0m0.052s
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Happy Cucumbering!
Copyright 2025 Daniel Miladinov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.