Kaocha support for Cucumber tests.
Cucumber is a "Behavior Driven Development" tool, a way to write "feature" tests in a human readable format known as Gherkin syntax.
This project adds Cucumber support to Kaocha, allowing to write and Cucumber tests using Clojure.
This assumes you already have Kaocha setup, including a tests.edn
and
bin/kaocha
wrapper.
Start by adding lambdaisland/kaocha-cucumber
to your project,
;; deps.edn
{:paths [,,,]
:deps {,,,}
:aliases
{:test {:extra-deps {lambdaisland/kaocha { ,,, }
lambdaisland/kaocha-cucumber { ,,, }}}}}
Create a directory which will contain your Cucumber tests (*.feature
files),
and one which will contain step definitions (*.clj
files).
mkdir -p test/features test/steps_definitions
Now add a Cucumber test suite to tests.edn
#kaocha/v1
{:tests [{:id :unit
:type :kaocha.type/clojure.test
:source-paths ["src"]
:test-paths ["test/unit"]}
{:id :features
:type :kaocha.type/cucumber
:source-paths ["src"]
:test-paths ["test/features"]
:cucumber/glue-paths ["test/steps_definitions"]
:cucumber/locale "de-DE" ; optional. Currently only for number
; format parsing, passed to
; java.util.Locale/forLanguageTag
}]}
Finally create a file test/features/coffeeshop.feature
with the following contents
Feature: Coffee shop order fulfilment
The coffee shop contains a counter where people can place orders.
Background:
Given the following price list
| Matcha Latte | 4.00 |
| Green Tea | 3.50 |
Scenario: Getting change
When I order a Matcha Latte
And pay with $5.00
Then I get $1.00 back
And run it
bin/kaocha features
Since the steps aren't implemented yet, Kaocha will tell you what you're missing.
$ bin/kaocha features
[(P)]
PENDING in test.features.coffeeshop/line-10 (test/features/coffeeshop.feature:10)
You can implement missing steps with the snippets below:
(When "I order a Matcha Latte" [state]
;; Write code here that turns the phrase above into concrete actions
(pending!))
(And "pay with ${double}" [state double1]
;; Write code here that turns the phrase above into concrete actions
(pending!))
(Then "I get ${double} back" [state double1]
;; Write code here that turns the phrase above into concrete actions
(pending!))
1 tests, 0 assertions, 1 pending, 0 failures.
Create a file test/step_definitions/coffeeshop_steps.clj
, copy the sample code
snippets over, and add a namespace declaration, pulling in
lambdaisland.cucumber.dsl
and clojure.test
.
(ns coffeeshop-steps
(:require [lambdaisland.cucumber.dsl :refer :all]
[clojure.test :refer :all]))
(Given "the following price list" [state table]
(assoc state
:price-list
(into {}
(map (fn [[k v]]
[k (Double/parseDouble v)]))
table)))
(When "I order a (.*)" [state product]
(update state :order conj product))
(And "pay with ${double}" [{:keys [price-list order] :as state} paid]
(doseq [product order]
(is (contains? price-list product)))
(let [total (apply + (map price-list order))]
(is (<= total paid))
(assoc state
:total total
:paid paid
:change (- paid total))))
(Then "I get ${double} back" [{:keys [change] :as state} expected]
(is (= expected change))
state)
Each step is followed by a pattern, a string which is interpreted as either a regular expression or a Cucumber expression, and then followed by an argument list (binding form) and a function body.
The first argument is always the current state. This is a map that is passed from one step to the next as it is executed. The first step receives an empty map, the next step receives the return value from the first step, etc.
Any remaining arguments correspond with capturing groups or "output parameters" in the pattern.
Inside the step definitions you use clojure.test
style assertions.
Here's the complete file structure of this example:
.
├── bin
│ └── kaocha
├── deps.edn
├── test
│ ├── features
│ │ └── coffeeshop.feature
│ └── step_definitions
│ └── coffeeshop_steps.clj
└── tests.edn
When running through a REPL, we need to disable the glue-cache. Otherwise, after we change the steps_definitions, we will still run the old steps.
The disable command is (reset! lambdaisland.cucumber.jvm/glue-cache nil)
.
Example:
Clojure 1.10.3
OpenJDK 64-Bit Server VM 17.0.6+10-LTS
Interrupt: Control+C
Exit: Control+D or (exit) or (quit)
user=> (require '[lambdaisland.cucumber.jvm])
nil
user=> (require '[kaocha.repl :as k])
nil
;; First time to run
user=> (k/run :features)
PENDING in coffeeshop/getting-change (test/features/coffeeshop.feature:10)
You can implement missing steps with the snippets below:
(Then "I get ${double} back" [state double1]
;; Write code here that turns the phrase above into concrete actions
(pending!))
1 tests, 2 assertions, 1 pending, 0 failures.
PENDING coffeeshop/getting-change (test/features/coffeeshop.feature:10)
#:kaocha.result{:count 1, :pass 2, :error 0, :fail 0, :pending 1}
;; Now, we change the steps_definitions
;; We need to disable the glue-cacahe before re-run
user=> (reset! lambdaisland.cucumber.jvm/glue-cache nil)
nil
user=> (k/run :features)
]
Randomized with --seed 279015433
FAIL in coffeeshop/getting-change (test/features/coffeeshop.feature:13)
I get $1.00 back
Expected:
100.0
Actual:
-100.0 +496.0
1 tests, 3 assertions, 1 failures.
#:kaocha.result{:count 1, :pass 2, :error 0, :fail 1, :pending 0}
Following standard Gherkin
syntax, you may add tags
by prefixing them with @
. For example, you could add a @customer-facing
tag
to the previous test/features/coffeeshop.feature
.
This would tag the whole feature with @customer-facing
:
@customer-facing
Feature: Coffee shop order fulfilment
This would tag a scenario with @customer-facing
:
@customer-facing
Scenario: Getting change
Then, you can use --focus-meta
or --skip-meta
in order to select only tagged
tests or skip them respectively:
bin/kaocha --focus-meta customer-facing
Data tables will be converted to a vector of vectors, other types will be passed on as-is.
To implement Custom Parameter
Types,
add a :cucumber/parameter-types
to your config.
#kaocha/v1
{:tests [{:id :cukes
:type :kaocha.type/cucumber
:source-paths ["src"]
:test-paths ["test/features" "test/support"]
:cucumber/glue-paths ["test/step_definitions"]
:cucumber/parameter-types
[#:cucumber.parameter
{:name "color"
:regexp "red|blue|yellow|green"
:class kaocha.cucumber.extra_types.Color
:transformer kaocha.cucumber.extra-types/parse-color}]}]}
Following keys are understood
:cucumber.parameter/name ;;=> string?
:cucumber.parameter/transformer ;;=> qualified-symbol?
:cucumber.parameter/regexp ;;=> string?
:cucumber.parameter/class ;;=> simple-symbol?
:cucumber.parameter/suggest? ;;=> boolean?
:cucumber.parameter/prefer-for-regexp-match? ;;=> boolean?
The existing cucumber-jvm-clojure
and lein-cucumber
are based on a very old
version of cucumber-jvm
, and appear to be unmaintained. Because of this
kaocha-cucumber
is built directly on top of cucumber-jvm
. Some of the code
in lambdaisland.cucumber.jvm
and lambdaisland.cucumber.dsl
is adapted from
these earlier projects.
If you are migrating existing code bases then note that the DSL syntax is different. Patterns are given as strings, not regular expressions, the first "state" argument is a new introduction, and certain argument types are coerced to Clojure types now.
If you have a code base of significant size based on these legacy projects then please open a ticket, we might be able to create a separate legacy DSL namespace that's compatible with old tests.
kaocha-cucumber is part of a growing collection of quality Clojure libraries created and maintained by the fine folks at Gaiwan.
Pay it forward by becoming a backer on our Open Collective, so that we may continue to enjoy a thriving Clojure ecosystem.
You can find an overview of our projects at lambdaisland/open-source.
Everyone has a right to submit patches to kaocha-cucumber, and thus become a contributor.
Contributors MUST
- adhere to the LambdaIsland Clojure Style Guide
- write patches that solve a problem. Start by stating the problem, then supply a minimal solution.
*
- agree to license their contributions as MPL 2.0.
- not break the contract with downstream consumers.
**
- not break the tests.
Contributors SHOULD
- update the CHANGELOG and README.
- add tests for new functionality.
If you submit a pull request that adheres to these rules, then it will almost certainly be merged immediately. However some things may require more consideration. If you add new dependencies, or significantly increase the API surface, then we need to decide if these changes are in line with the project's goals. In this case you can start by writing a pitch, and collecting feedback on it.
*
This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution.
**
As long as this project has not seen a public release (i.e. is not on Clojars)
we may still consider making breaking changes, if there is consensus that the
changes are justified.
Copyright © 2019-2021 Arne Brasseur and Contributors
Licensed under the term of the Eclipse Public License 1.0, see LICENSE.txt.