A more idiomatic and up-to-date binding of JVM cucumber to Clojure. No global state, and suitable for repl-based development. Also lets you write proptests with Gherkin syntax.
Add it to your deps.edn
{:deps {auxon/clj-cucumber {:git/url "https://github.com/auxoncorp/clj-cucumber"
:sha "<>"}}}
Make a feature file, and put it somewhere in your source tree. test/features
is a handy location.
Feature: A passing feature
Scenario: Successful test
Given some setup
When I do a thing
Then the setup happened
And the thing happened
Make a regular clojure test, and call run-cucumber
from it.
(ns cucumber-test
(:require [clojure.test :refer :all]
[auxon.clj-cucumber :refer [run-cucumber step]]))
(def test-state (atom {}))
(def steps
[])
(deftest cucumber
(is (= 0 (run-cucumber "test/features/passing.feature" steps))))
Run your test in whatever way you do normally, then see what was printed. Copy the step templates into your list of steps.
clj -A:test:runner
<snip> You can implement missing steps with the snippets below: (step :Given #"^some setup$" (fn some-setup [state] (comment Write code here that turns the phrase above into concrete actions) (throw (cucumber.api.PendingException.)))) (step :When #"^I do a thing$" (fn I-do-a-thing [state] (comment Write code here that turns the phrase above into concrete actions) (throw (cucumber.api.PendingException.)))) (step :Then #"^the setup happened$" (fn the-setup-happened [state] (comment Write code here that turns the phrase above into concrete actions) (throw (cucumber.api.PendingException.)))) (step :Then #"^the thing happened$" (fn the-thing-happened [state] (comment Write code here that turns the phrase above into concrete actions) (throw (cucumber.api.PendingException.))))
Fill out the body as appropriate.
(def steps
[(step :Given #"^some setup$"
(fn some-setup [_]
{:setup-happened true}))
(step :When #"^I do a thing$"
(fn I-do-a-thing [state]
(assoc state :thing-happened true)))
(step :Then #"^the setup happened$"
(fn the-setup-happened [state]
(assert (:setup-happened state))
state))
(step :Then #"^the thing happened$"
(fn the-thing-happened [state]
(assert (:thing-happened state))
state))])
NB You should use assert
in your step defs rather than clojure.test’s =
macro, so the cucumber test runner can observe the assertion failures.
clj -A:test:runner
Running tests in #{"test"} Testing cucumber-test Feature: A passing feature Scenario: Successful test # test/features/my.feature:2 Given some setup # cucumber_test.clj:8 When I do a thing # cucumber_test.clj:13 Then the setup happened # cucumber_test.clj:17 And the thing happened # cucumber_test.clj:21 1 Scenarios (1 passed) 4 Steps (4 passed) 0m0.019s Ran 1 tests containing 1 assertions. 0 failures, 0 errors.
Steps are maps. The step
macro is provided to seamlessly populate the line
number information in them, which the cucumber library uses when printing
results.
The first parameter to each step function is a state variable. This is passed between steps; the return value of one step is the state passed to the next.
As with most cucumber implementations, regex subgroups are turned into paramters to your step functions.
(def steps
[(step :When #"^I do (\d+) things$"
(fn I-do-int-things [x]
...))])
You can add hooks, too, alongside your steps. Use the hook
macro to make them.
(def steps
;; these happen before and after the scenario
[(hook :before (fn before-hook [state] ...))
(hook :after (fn after-hook [state] ...))
;; these happen before and after each step
(hook :before-step (fn before-step-hook [state] ...))
(hook :after-step (fn after-step-hook [state] ...))
(step :Given ...)])
As with steps, all hook functions are passed the state, and their return value is used as the new state.
You can write generative (property-based) tests with your gherkin specs! This is
effectively a special mode; the auxon.clj-cucumber.generative
namespace
defines a before-hook
and after-hook
that use the environment to accumulate
generators and properties as the feature executes, then check them at the end.
You can use the generator
, property
, and step
macros defined there to
write the test backend. Note that when you do this, all the test execution is
actually happening inside the after hook, so the cucumber test runner can’t
localize the failures as well.
See test/auxon/clj_cucumber/generative_test.clj and test/auxon/clj_cucumber/features/generative.feature for an example of how to do this.
Generative Testing Tips
- In generative test
property
functions, simply return a bool to indicate if the property holds, instead of using an assertion. - Don’t mix and match regular steps with generative steps.
clj -A:test:runner
You may see some console traffic that looks like a failure; that’s because there are tests which check that failures are caught, and they print. It’s expected.
Copyright © 2019 Auxon Corporation
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.