/clj-cucumber

A more idiomatic binding of JVM cucumber to Clojure

Primary LanguageClojureEclipse Public License 1.0EPL-1.0

auxon.clj-cucumber

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.

Usage

Add the dependency

Add it to your deps.edn

{:deps {auxon/clj-cucumber {:git/url "https://github.com/auxoncorp/clj-cucumber"
                            :sha "<>"}}}

Make a feature file

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 test file

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 the tests and copy the template step defs

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 in the blank

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.

Happily run your now-passing tests

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

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.

Other Features

Parameters

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]
           ...))])

Hooks

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.

Generative tests

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.

Testing this code

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.

License

Copyright © 2019 Auxon Corporation

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.