Skip to content

An idiomatic Clojure wrapper for the latest version of cucumber-jvm, inspired by auxoncorp/clj-cucumber

License

Notifications You must be signed in to change notification settings

danielmiladinov/burpless

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

danielmiladinov/burpless

An idiomatic Clojure wrapper around cucumber-jvm, for writing Cucumber feature tests. inspired by auxoncorp/clj-cucumber.

Library name inspired by Roman Ostash.

Clojars Project

Usage

Add the Dependency

Deps:

Add it to your deps.edn:

{:deps {net.clojars.danielmiladinov/burpless {:mvn/version "0.1.0"}}}

Lein/Boot:

Add it to your project.clj:

[net.clojars.danielmiladinov/burpless "0.1.0"]

Write a Feature File

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:
    """
    {: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.

Write a Test File

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 tests and copy the step definition snippets from the output

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:

Undefined scenarios:
file:///path/to/my-first.feature:3 # Learning to use Burpless

1 Scenarios (1 undefined)
5 Steps (4 skipped, 1 undefined)
0m0.041s


You can implement missing steps with the snippets below:

(step :Given "I have a string value of {string} under the :message key in my state"
      (fn i_have_a_string_value_of_under_the_message_key_in_my_state [state ^String string]
        ;; 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 :stars key in my state"
      (fn i_have_a_long_value_of_under_the_stars_key_in_my_state [state ^Integer int1]
        ;; 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 i_have_a_table_of_the_following_high_and_low_temperatures [state ^io.cucumber.datatable.DataTable dataTable]
        ;; Write code here that turns the phrase above into concrete actions
        ;; Be sure to also adorn your step function with the ^:datatable metadata
        ;; in order for the runtime to properly identify it and pass the datatable argument
        (throw (io.cucumber.java.PendingException.))))

(step :When "I am ready to check my state"
      (fn i_am_ready_to_check_my_state [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 my_state_should_be_equal_to_the_following_clojure_literal [state ^String docString]
        ;; 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:35)
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

Copy and Paste the Step Functions

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 :message key in my state"
      (fn i_have_a_string_value_of_under_the_message_key_in_my_state [state ^String string]
        ;; 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 :stars key in my state"
      (fn i_have_a_long_value_of_under_the_stars_key_in_my_state [state ^Integer int1]
        ;; 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 i_have_a_table_of_the_following_high_and_low_temperatures [state ^io.cucumber.datatable.DataTable dataTable]
        ;; Write code here that turns the phrase above into concrete actions
        ;; Be sure to also adorn your step function with the ^:datatable metadata
        ;; in order for the runtime to properly identify it and pass the datatable argument
        (throw (io.cucumber.java.PendingException.))))

(step :When "I am ready to check my state"
      (fn i_am_ready_to_check_my_state [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 my_state_should_be_equal_to_the_following_clojure_literal [state ^String docString]
        ;; Write code here that turns the phrase above into concrete actions
        (throw (io.cucumber.java.PendingException.))))

Pay attention to the comments embedded in the step functions; if you are following closely, you'll see some things that may not immediately make sense:

  • Why does each step function have a first argument called state?
  • What is this ^io.cucumber.datatable.DataTable dataTable argument all about?
  • What does this comment mean?
 ;; Be sure to also adorn your step function with the ^:datatable metadata
 ;; in order for the runtime to properly identify it and pass the datatable argument

Let's try to answer these questions in the next section.

Burpless Step Functions

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
  • 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.)
  • 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 are provided as additional arguments to the function.

Doc Strings and Data Tables

Burpless' implementation of the Cucumber Backend interface is responsible for adding StepDefinitions to the Glue instance provided to it during the call to loadGlue(), and they must return ParameterInfo lists that match what the Cucumber runtime discovered while parsing the feature file(s) into Gherkin steps, or else Cucumber will report that step as undefined.

The current design of the Cucumber JVM library tries to make it very easy to identify the code that should run for a particular Gherkin step - assuming that your JVM language is strongly typed, and has excellent annotation support. Just annotate your methods with the appropriate annotation(s), and the cucumber runtime does the rest!

Coming from Clojure, that's two strikes against us.

Using Cucumber Expressions, it's fairly easy to extract output parameter info from the pattern itself, but that doesn't help us with DataTable or DocString parameters. They aren't part of the pattern, but follow in the next line(s) in the feature file. While there are ways to reflectively get the type hints on Clojure function parameters, these only seem to work for top-level functions defined with defn, not for the inline functions defined with fn and passed to the step macro. The parameter info for each StepDefinition has to come from somewhere, as parameterInfos() method takes no arguments.

If there were another way to make this easier to do in Clojure, I would do it. But since I haven't yet found it, or it's not possible, then, for step functions intended to match steps that are following by a DataTable then you must tag your step function with the ^:datatable metadata:

(step :Given "I want to receive a DataTable parameter to my step function"
      ^:datatable
      (fn [state ^io.cucumber.datatable.DataTable dataTable]
        ;; Do something interesting with state and dataTable, returning an updated state
        ))

Similarly, for step functions intended to match steps that are followed by a DocString, you must tag your step function with the ^:docstring metadata:

(step :Given "I want to receive a DocString parameter to my step function"
      ^:docstring
      (fn [state ^io.cucumber.docstring.DocString docString]
        ;; Do something interesting with the state and docString, returning an updated state
        ))

Update the Step Functions to Pass the Test

While you might be able to come up with something slightly different, here's one possible implementation of step functions that makes the test pass:

(ns my-first-feature-test
  (:require [burpless :refer [run-cucumber step]]
            [clojure.string :as str]
            [clojure.test :refer [deftest is]])
  (:import (io.cucumber.datatable DataTable)
           (io.cucumber.docstring DocString)
           (java.lang.reflect Type)))

(def steps
  [(step :Given "I have a string value of {string} under the :message key in my state"
         (fn [state ^String message]
           (assoc state :message message)))

   (step :Given "I have a long value of {long} under the {word} key in my state"
         (fn [state ^Long stars ^String keyword-name]
           (assoc state (keyword (str/replace keyword-name #":" "")) stars)))

   (step :Given "I have a table of the following high and low temperatures:"
         ^:datatable
         (fn [state ^DataTable dataTable]
           (assoc state :highs-and-lows (.asLists dataTable ^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:"
         ^:docstring
         (fn [actual-state ^DocString docString]
           (let [expected-state (read-string (.getContent docString))]
             (is (= expected-state actual-state)))))])

(deftest my-first-feature
  (is (= 0 (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:10
  And I have a long value of 5 under the :stars key in my state                        # my_first_feature_test.clj:14
  And I have a table of the following high and low temperatures:                       # my_first_feature_test.clj:18
    | 81 | 49 |
    | 88 | 54 |
    | 76 | 56 |
    | 70 | 48 |
    | 81 | 55 |
  When I am ready to check my state                                                    # my_first_feature_test.clj:23
  Then my state should be equal to the following Clojure literal:                      # my_first_feature_test.clj:27

1 Scenarios (1 passed)
5 Steps (5 passed)
0m0.037s



Ran 1 tests containing 2 assertions.
0 failures, 0 errors.

Happy Cucumbering!

License

License

Copyright 2023 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.

About

An idiomatic Clojure wrapper for the latest version of cucumber-jvm, inspired by auxoncorp/clj-cucumber

Topics

Resources

License

Stars

Watchers

Forks

Packages