Scenari
Clojure BDD library - Executable Specification with Behavior-Driven Development
Install / Use
/learn @defsquare/ScenariREADME
Scenari - Executable Specification / BDD in Clojure
Scenari is an Executable Specification Clojure library aimed at writing and executing usage scenarios following the Behavior-Driven Development - BDD - style. It has an external DSL, following the gherkin grammar (in short: Given/When/Then), and execute each scenario's steps with associated Clojure code.
Installation
;;add this dependency to your project.clj file
[io.defsquare/scenari "2.0.2"]
;;or deps.edn
{
io.defsquare/scenari {:mvn/version "2.0.2"}
}
;;then in your ns statement
(:require [scenari.v2.core :as scenari :refer [defgiven defwhen defthen deffeature]])
Basic Usage
Write Scenarios in plain text
First, write your scenarios in plain text using the Gherkin grammar in a file or String : You can add a "narrrative" for all your scenarios with the story syntax at the beginning of the story file (As a role I want to do something In order to get value)
Scenario: create a new product
# this is a comment
When I create a new product with name "iphone 6" and description "awesome phone"
Then I receive a response with an id 56422 and a location URL
# this a second comment
# on two lines
When I invoke a GET request on location URL
Then I receive a 200 response
Scenario: get product info
When I invoke a GET request on location URL
Then I receive a 200 response
Declaring a specification
(require 'scenari.v2.core :refer [deffeature])
(deffeature my-specification "./path/to/feature/file") ;; define deftest bound to symbol 'my-specification', put the specification as clojure data-structure in metadata and return the specification
;;=>
;;{:scenarios [{:id "0ef9b8a9-e035-4ae2-96c4-662c0b8988de",
;; :pre-run (),
;; :post-run (),
;; :scenario-name " create a new product",
;; :steps [{:sentence-keyword :when,
;; :sentence "I create a new product with name \"iphone 6\" and description \"awesome phone\"",
;; :raw "When I create a new product with name \"iphone 6\" and description \"awesome phone\"",
;; :params [{:type :value, :val "iphone 6"} {:type :value, :val "awesome phone"}],
;; :order 0,
;; :glue nil}
;; ...steps]}
;; ...scenarios ],
;; :pre-run ()}
Write glue-code
Then write the code that will get executed for each scenario steps:
(require 'scenari.v2.core :refer [defwhen defthen])
(defwhen "I create a new product with name {string} and description {string}"
[_ name desc]
(println "executing my product creation function with params " name desc)
(let [id (UUID/next.)]
{:id (UUID/next. )
:name name
:desc desc
:qty (rand-int 50)
:location-url (str "http://example.com/product/" id)}))
(defthen "I receive a response with an id {string}"
[_ id]
(println (str "executing the assertion that the product has been created with the id " id))
id)
Tips: you can get a function snippet generated for you when executing the spec without step function. Think about enclosing with quote 'your data' in step sentence to get them detected by the parser and it'll generate a step function skeleton in the output with the correct sentence matcher group. Example:
Executing the specification with the step sentence without any matching function:
When I create a new product with name "iphone 6" and description "awesome phone"
will generate in the stdout the following step function skeleton:
Missing step for : When I create a new product with name "iphone 6" and description "awesome phone"
(defwhen "I create a new product with name {string} and description {string}" [state arg0 arg1] (do "something"))
how to get data from the scenario into your step function
Every group the sentence matcher will find (everything enclosed in curly braces in your sentence matcher) will be transmitted as a string to your step function params with the same left-to-right order, BUT the data is first evaluated as clojure.edn data string (see clojure.edn/read-string) and IF it is a Clojure data structure ((coll? evaluated-data) returns true), THEN it will be transmitted evaluated as a param to the step function.
Tips: the map will be detected by the parser and it'll generate a step function skeleton in the output with the correct sentence matcher.
Execute scenario(s)
There is three-way to execute scenarios, depending on your situation
Tree execution
Declaring your specification using scenari.v2.core/deffeature returns the parsed specification as clojure data structure. By using scenari.v2.core/run-scenario, the specification as data will be ran
(require 'scenari.v2.core :refer [run-feature run-features])
(run-feature #'my-specification)
;;OR
(require 'scenari.v2.core :refer [run-scenarios])
(run-features #'my-specification)
The execution report will be returned, rely on same clojure data-structure returned by scenari.v2.core/deffeature. Will set :
- final
:statusof scenario(s) execution - step
:statusaspendingwhen not executed,failedwhen assertions fail or exception thrown,successat last - step
:input-stateas the value returned by the previous step executed (empty map for the first one) - step
:output-stateas the value returned by the current step
This method is useful for debugging.
Clojure-test execution
Use clojure-test reporting system by printing execution.
(require 'scenari.v2.test :refer [run-feature])
(run-feature #'my-specification)
;; ________________________
;; Feature :
;;
;; Testing scenario :
;; When I create a new product with name "iphone 6" and description "awesome phone" (from /")
;; Step failed
;; create a new product failed at step of
;;
;; Testing scenario :
;; When I invoke a GET request on location URL (from scenari.v2.glue/"I invoke a GET request on location URL")
;; =====> {:kix "lol"}
;; Then I receive a 200 response (from /"")
;; Step failed
;; get product info failed at step of
;;
;; ________________________
;;
Useful to integrate a feature in a clojure test namespace
Kaocha runner
Kaocha is a test runner and handle test phase lifecycle.
By defining a test type in your kaocha configuration file (tests.edn by default) like this
#kaocha/v1
{:tests [{:id :scenario
:type :kaocha.type/scenari
:kaocha/source-paths ["src"]
:kaocha/test-paths ["test/scenario"]
:kaocha.type.scenari/glue-paths ["test/scenario/glue"]}]}
You are able to launch your scenario using kaocha repl utility function
(require 'kaocha.repl :as krepl)
(krepl/run :scenario)
;; Testing scenario : create a new product
;; When I invoke a GET request on location URL (from scenari.v2.glue/"I invoke a GET request on location URL")
;; When I create a new product with name "iphone 6" and description "awesome phone" with properties (from scenari.v2.glue/"I create a new product with name \"(.*)\" and description \"(.*)\" with properties")
;; Then I receive a response with an id 56422 (from scenari.v2.glue/"I receive a response with an id 56422")
;; Then a location URL (from scenari.v2.glue/"a location URL")
;;
;;
;; 1 tests, 1 assertions, 0 failures.
;; => #:kaocha.result{:count 1, :pass 1, :error 0, :fail 0, :pending 0}
Suitable when using kaocha to manage test lifecycle.
Using hooks
By providing an options maps in scenari.v2.core/deffeature, you can specify function which execute :
:pre-runbefore feature execution:post-runafter feature executed:pre-scenario-runbefore each scenario execution:post-scenaro-runafter each scenario executed Example:
(require 'scenari.v2.core :refer [deffeature])
(defn before-all [] (prn "init feature components"))
(defn before-each [] (prn "init scenario components"))
(defn after-each [] (prn "clean scenario side effects"))
(defn clean [] (prn "reset and shut down components"))
(deffeature my-specification "./path/to/feature/file"
{:pre-run [#'before-all]
:pre-scenario-run [#'before-each]
:post-scenario-run [#'after-each]
:post-run [#'clean]})
Provide an initial state
For each scenario execution, an initial state can be provided within the options map of deffeature.
Example:
(deffeature my-specification "./path/to/feature/file"
{:default-scenario-state {:foo "bar"}})
By default, the scenario state is an empty map {}.
Documentation
Development Workflow
The Development Workflow Guide provides a comprehensive overview of the full development cycle when using Scenari. It covers writing feature files, defining features in code, implementing step definitions, and understanding how execution works.
Internal Feature Structure
The Feature Structure Documentation provides a detailed explana
