├── deploy.sh ├── test.sh ├── scenari.sketch ├── .gitignore ├── doc ├── intro.md ├── feature-structure.md └── development-workflow.md ├── resources ├── calculator.feature ├── utilisateurs.story ├── product-catalog.feature ├── scenari.feature ├── atm.feature └── remember-me.feature ├── test └── scenari │ └── v2 │ ├── other_glue │ └── glue.clj │ ├── example.feature │ ├── some_glue_ns.clj │ ├── core_test.clj │ ├── step_test.clj │ ├── feature_test.clj │ ├── glue_test.clj │ └── parsing_test.clj ├── src ├── scenari │ ├── meta.clj │ ├── v2 │ │ ├── step.clj │ │ ├── glue.clj │ │ ├── test.clj │ │ ├── parser.clj │ │ └── core.clj │ └── utils.clj └── kaocha │ └── type │ └── scenari.clj ├── CHANGELOG.md ├── tests.edn ├── LICENSE ├── release.sh ├── script └── build.clj ├── deps.edn ├── pom.xml ├── scenari.svg └── README.md /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | clojure -T:build deploy 4 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | clojure -A:test -m kaocha.runner "$@" 4 | -------------------------------------------------------------------------------- /scenari.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defsquare/scenari/HEAD/scenari.sketch -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .* 11 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to spexec 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /resources/calculator.feature: -------------------------------------------------------------------------------- 1 | Feature: a calculator engine 2 | 3 | Scenario: addition 4 | Given I type 2 and 3 5 | When I press add 6 | Then the result should be 5 -------------------------------------------------------------------------------- /test/scenari/v2/other_glue/glue.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.other-glue.glue 2 | (:require [clojure.test :refer :all] 3 | [scenari.v2.core :as v2])) 4 | 5 | (v2/defgiven #"My duplicated step in others ns" [state] 6 | state) -------------------------------------------------------------------------------- /src/scenari/meta.clj: -------------------------------------------------------------------------------- 1 | ;; This code was automatically generated by the 'metav' library. 2 | (ns scenari.meta) 3 | 4 | (def module-name "scenari") 5 | (def path ".") 6 | (def version "2.0.2") 7 | (def tag "v2.0.2") 8 | (def generated-at "2025-09-16T13:00:00Z") 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | # [Unreleased] # 5 | 6 | # [1.4.4] - 2019-09-18 7 | 8 | Add insta parse regex to handle unicode characters, numerics and punctuation 9 | https://github.com/jgrodziski/scenari/pull/8 10 | 11 | # [1.4.0] - 2019-05-17 # 12 | 13 | ## Added ## 14 | 15 | Examples table as input step param 16 | -------------------------------------------------------------------------------- /test/scenari/v2/example.feature: -------------------------------------------------------------------------------- 1 | Feature: foo bar kix 2 | 3 | Scenario: create a new product 4 | When I invoke a GET request on location URL 5 | # this is a comment 6 | When I create a new product with name "iphone 6" and description "awesome phone" with properties 7 | | size | weight | 8 | | 6 | 2 | 9 | Then I receive a response with an id 56422 10 | Then a location URL 11 | 12 | Scenario: another 13 | Given I foo 14 | 15 | Scenario: scenario with doc string 16 | Given a doc string 17 | """ 18 | This is markdown 19 | """ 20 | -------------------------------------------------------------------------------- /resources/utilisateurs.story: -------------------------------------------------------------------------------- 1 | Scénario : Créer un utilisateur 2 | Etant donné que l'utilisateur n'existe pas 3 | Quand je crée l'utilisateur 4 | Alors l'utilisateur existe dans le repository des utilisateurs 5 | Exemples : 6 | |mail1 | 7 | |user1@company.com | 8 | |user2@company.com | 9 | 10 | Scénario : Associer un profil fournisseur à un utilisateur 11 | Etant donné que l'utilisateur existe 12 | Etant donné que le profil fournisseur existe 13 | Quand j'associe le profil fournisseur à l'utilisateur 14 | Alors l'utilisateur a le profil fournisseur -------------------------------------------------------------------------------- /resources/product-catalog.feature: -------------------------------------------------------------------------------- 1 | Narrative: 2 | As a product manager 3 | I want to add a new product to the catalog 4 | So that I fill the catalog with interesting product 5 | 6 | Scenario: create a new product 7 | # this is a comment 8 | When I create a new product with name "iphone 6" and description "awesome phone" 9 | Then I receive a response with an id and a location URL 10 | # this a second comment 11 | # on two lines 12 | When I invoke a GET request on location URL 13 | Then I receive a 200 response 14 | 15 | Scenario: create a new product with a data map 16 | When I create a new product with '{:description "awesome phone", :name "iphone 6"}' 17 | 18 | Scenario: get product info 19 | #test 20 | When I invoke a GET request on location URL 21 | Then I receive a 200 response 22 | -------------------------------------------------------------------------------- /resources/scenari.feature: -------------------------------------------------------------------------------- 1 | Scenario: a scenario that test spexec using spexec 2 | Given the step function: (defgiven (re-pattern "this scenario in a file named '(.*)'$") [_ feature-file-name] [feature-file-name]) 3 | Given the step function: (defwhen (re-pattern "I run the scenarios with '(.+)'$") [prev-ret my-data] (conj prev-ret (str "processed" my-data))) 4 | Given the step function: (defthen (re-pattern "I should get '(.+)' from scenario file '(.*)' returned from the previous when step$") [prev-ret expected-data scenario-file] (clojure.test/is (= (last prev-ret) expected-data))(clojure.test/is (= (first prev-ret) scenario-file))) 5 | Given this scenario in a file named 'resources/spexec.feature' 6 | When I run the scenarios with 'mydatavalues' 7 | Then I should get 'processedmydatavalues' from scenario file 'resources/spexec.feature' returned from the previous when step 8 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :scenario 3 | :type :kaocha.type/scenari 4 | :kaocha/source-paths ["src"] 5 | :kaocha/test-paths ["test/scenari/v2"] 6 | :kaocha.type.scenari/glue-paths ["scenari/v2"]} 7 | 8 | {:id :unit 9 | :type :kaocha.type/clojure.test 10 | :source-paths ["src"] 11 | :test-paths ["test"] 12 | :ns-patterns ["scenari.*-test"] 13 | :kaocha.filter/skip-meta [:scenari/feature-test :integration]} 14 | ] 15 | :kaocha/reporter kaocha.report/documentation} 16 | -------------------------------------------------------------------------------- /test/scenari/v2/some_glue_ns.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.some-glue-ns 2 | (:require [clojure.test :refer :all] 3 | [scenari.v2.core :as v2])) 4 | 5 | 6 | (v2/defwhen "I create a new product with name {string} and description {string} with properties" [state arg0 arg1 arg2] 7 | (is (= 1 1)) 8 | {:foo "bar"}) 9 | 10 | (v2/defthen "I receive a response with an id {number}" [state n] 11 | (is (= 56422 n)) 12 | state) 13 | 14 | (v2/defthen "a location URL" [state] 15 | (do "assert the result of when step") 16 | state) 17 | 18 | (v2/defwhen "I invoke a GET request on location URL" [state] 19 | (is (= 1 1)) 20 | (assoc state :kix "lol")) 21 | 22 | 23 | (v2/defgiven "My duplicated step in other ns and feature ns" [state] 24 | state) 25 | 26 | (v2/defgiven "My duplicated step in others ns" [state] 27 | state) -------------------------------------------------------------------------------- /resources/atm.feature: -------------------------------------------------------------------------------- 1 | Feature: cash withdrawal 2 | As a card holder of a card emitted by a french bank 3 | I want to withdraw cash in France 4 | In order to get cash rapidly anywhere and anytime 5 | 6 | Scenario: withdrawal with a payment card at an ATM - success withdrawal accepted 7 | Given the card holder "Jeremie" has the card 1234567890123456 with a 1000 € balance 8 | When the card holder withdraw 200 € at the ATM rue de l'université 9 | Then he gets 200 € in cash 10 | Then the account balance is 800 € 11 | 12 | Scenario: withdrawal with a payment card at an ATM - failure insufficient balance 13 | Given the card holder "Jeremie" has the card 1234567890123456 with a 50 € balance 14 | # rule insufficient_balance 15 | When the card holder withdraw 200 € at the ATM rue de l'université 16 | Then he gets the message "insufficient balance" 17 | Then the account balance is 50 € 18 | 19 | #Rule: insufficient_balance 20 | # When 21 | # Then 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/scenari/v2/step.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.step 2 | (:require [clojure.string :as string] 3 | [instaparse.core :as insta] 4 | [scenari.v2.parser :as parser])) 5 | 6 | (defn- extract-params-as-args [params] 7 | (str "[state " (string/join " " (map-indexed (fn [idx _] (str "arg" idx)) params)) "]")) 8 | 9 | (defn generate-step-fn 10 | "return a string representing a spexec macro call corresponding to the sentence step" 11 | [step-sentence] 12 | (let [{:keys [raw params]} step-sentence 13 | sentence-ast (parser/step raw) 14 | [_ [step-type] & sentence-elements] sentence-ast] 15 | (if (insta/failure? sentence-ast) 16 | (do (prn (insta/get-failure sentence-ast)) (throw (ex-info (:reason (insta/get-failure sentence-ast)) {:parsed-text step-sentence})))) 17 | (str (case step-type 18 | :given "(defgiven \"" 19 | :and "(defand \"" 20 | :when "(defwhen \"" 21 | :then "(defthen \"" 22 | "(defwhen \"") 23 | (apply str (map (fn [[what? data]] 24 | (case what? 25 | :words data 26 | :string "{string}" 27 | :number "{number}" 28 | "test")) sentence-elements)) 29 | "\" " 30 | (extract-params-as-args params) 31 | (case step-type 32 | :given " (do \"setup or assert correct tested component state\"))" 33 | :when " (do \"something\"))" 34 | :then " (do \"assert the result of when step\"))" 35 | " (do \"something\"))")))) 36 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RELEASE_LEVEL=$1 4 | MODULE_NAME=${PWD##*/} 5 | echo "Release \"$MODULE_NAME\" with level '$RELEASE_LEVEL'" 6 | tag=$(clj -M:release $RELEASE_LEVEL --spit --output-dir src --namespace scenari.meta --formats clj) 7 | 8 | if [ $? -eq 0 ]; then 9 | echo "Successfully released \"$MODULE_NAME\" to $tag" 10 | else 11 | echo "Fail to release \"$MODULE_NAME\"!" 12 | exit 1 13 | fi 14 | #################################################### 15 | # build jar # 16 | #################################################### 17 | source ./build.sh 18 | 19 | #################################################### 20 | # # 21 | # Clojars uploading stuff (easier with Maven) # 22 | # # 23 | #################################################### 24 | 25 | if [[ $tag =~ v(.+) ]]; then 26 | newversion=${BASH_REMATCH[1]} 27 | else 28 | echo "unable to parse tag $tag" 29 | exit 1 30 | fi 31 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$newversion 2>&1 > /dev/null 32 | 33 | if [ $? -eq 0 ]; then 34 | echo "Successfully set new version of \"$MODULE_NAME\"'s pom to $newversion" 35 | else 36 | echo "Fail to set new version of \"$MODULE_NAME\"'s pom!" 37 | exit 1 38 | fi 39 | 40 | # mvn deploy 2>&1 > /dev/null 41 | 42 | ARTIFACT_NAME=$(clj -M:artifact-name) 43 | ARTIFACT_ID=$(echo "$ARTIFACT_NAME" | cut -f1) 44 | ARTIFACT_VERSION=$(echo "$ARTIFACT_NAME" | cut -f2) 45 | 46 | mvn deploy 47 | 48 | if [ $? -eq 0 ]; then 49 | echo "Successfully deployed \"$MODULE_NAME\" version $newversion to clojars" 50 | else 51 | echo "Fail to deploy \"$MODULE_NAME\" to clojars!" 52 | exit 1 53 | fi 54 | -------------------------------------------------------------------------------- /resources/remember-me.feature: -------------------------------------------------------------------------------- 1 | Narrative: 2 | As a user 3 | I want to login 4 | In order to access to protected resource 5 | 6 | Scenario: successful authentication with remember-me worflow and remember-me checkbox unchecked 7 | Given a running webapp and a user storage containing {:username "john" :password "test-password"} and no remember-me token 8 | When the user login (POST to "/login" URL with form params username=john and password=test-password) 9 | Then the user should be authenticated (http response 200 and welcome page is displayed) 10 | 11 | Scenario: failed authentication with remember-me workflow and no remember-me checkbox unchecked 12 | Given a running webapp and a user storage containing {:username "john" :password "test-password"} and no remember-me token 13 | When the user login (POST to "/login" URL with form params username=john and password=incorrect) 14 | Then the user should not be authenticated (http response 403 and login page still displayed with error message) 15 | 16 | Scenario: successful authentication with remember-me worflow and remember-me checkbox checked 17 | Given a running webapp and a user storage containing {:username "john" :password "test-password"} and no remember-me token 18 | When the user login (POST to "/login" URL with form params username=john and password=test-password and remember-me=true) 19 | Then the user should be authenticated (http response 200 and welcome page is displayed) 20 | Then the http response should contain a cookie named "remember-me" 21 | 22 | Scenario: successful authentication with remember-me token 23 | Given the user session expires on the server 24 | When the user perform an http request on a protected resource (welcome page) with the token cookie 25 | Then the user should be authenticated (http response 200 and welcome page displayed) 26 | -------------------------------------------------------------------------------- /test/scenari/v2/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.core-test 2 | (:require [clojure.test :as t :refer [is]] 3 | [scenari.v2.core :as v2] 4 | [scenari.v2.test :as sc-test] 5 | [kaocha.type.scenari] 6 | [kaocha.repl :as krepl] 7 | [testit.core :refer :all])) 8 | 9 | (t/deftest find-sentence-params-test 10 | (t/testing "finding parameters in sentence" 11 | (is (= (v2/find-sentence-params "Given an id 1234") [{:type :value, :val 1234}]) "should return number value") 12 | (is (= (v2/find-sentence-params "Given an id \"1234\"") [{:type :value, :val "1234"}]) "should return string value") 13 | (is (= (v2/find-sentence-params "Given an id abc") []) "should return no parameters") 14 | (is (= (v2/find-sentence-params "Given an id 1234 and \"1234\" ") [{:type :value, :val 1234} {:type :value, :val "1234"}]) "should return multiple value"))) 15 | 16 | (v2/defgiven #"My duplicated step in other ns and feature ns" [state] 17 | state) 18 | 19 | (t/deftest deffeature-macro-test 20 | (t/testing "macro definition taking different feature structure" 21 | (t/is (some? (macroexpand '(v2/deffeature example-feature "test/scenari/v2/example.feature")))) 22 | (t/is (some? (macroexpand '(v2/deffeature example-feature (slurp "test/scenari/v2/example.feature"))))) 23 | (t/is (some? (macroexpand '(v2/deffeature example-feature (first (vector (slurp "test/scenari/v2/example.feature"))))))) 24 | (t/is (some? (macroexpand '(v2/deffeature (symbol (str "example-feature")) (first (vector (slurp "test/scenari/v2/example.feature"))))))))) 25 | 26 | (comment 27 | (remove-ns 'scenari.v2.core-test) 28 | (meta #'scenari.v2.core-test/my-feature) 29 | (v2/run-features) 30 | (v2/run-features #'scenari.v2.core-test/my-feature) 31 | (sc-test/run-features #'scenari.v2.core-test/my-feature) 32 | 33 | (t/run-tests) 34 | 35 | (krepl/test-plan) 36 | (krepl/run-all) 37 | (krepl/run :scenario)) -------------------------------------------------------------------------------- /script/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require 3 | [clojure.tools.build.api :as b] 4 | [deps-deploy.deps-deploy :as dd] 5 | [scenari.meta :refer [version tag]])) 6 | 7 | (def lib-name 'io.defsquare/scenari) 8 | (def jar-content "target/classes") 9 | (def basis (b/create-basis {:project "deps.edn"})) 10 | (def jar-file (format "target/%s-%s.jar" (name lib-name) version)) 11 | 12 | (defn clean "Clean target dir" [_] 13 | (println "Clean target directory...") 14 | (b/delete {:path "target"})) 15 | 16 | (defn jar "Build jar and pom into target dir" [_] 17 | (clean nil) 18 | (println "Copy sources/resources...") 19 | (b/copy-dir {:src-dirs ["src" "resources"] 20 | :target-dir jar-content}) 21 | (println "Write pom into target/META-INF...") 22 | (b/write-pom {:class-dir jar-content 23 | :lib lib-name 24 | :version version 25 | :basis basis 26 | :src-dirs ["src"] 27 | :scm {:connection "scm:git:git://github.com:defsquare/scenari.git" 28 | :developerConnection "scm:git:ssh://github.com:defsquare/scenari.git" 29 | :url "https://github.com/defsquare/scenari" 30 | :tag tag}}) 31 | (println "Build jar...") 32 | (b/jar {:class-dir jar-content 33 | :jar-file jar-file}) 34 | 35 | (println "Install jar and pom to local maven repository...") 36 | (b/install {:class-dir jar-content 37 | :lib lib-name 38 | :version version 39 | :basis basis 40 | :src-dirs ["src"] 41 | :jar-file jar-file})) 42 | 43 | (defn deploy "Deploy the JAR to Clojars." [_] 44 | (let [pom-path (b/pom-path {:class-dir jar-content 45 | :lib lib-name})] 46 | (println (format "Deploy jar %s and pom %s to clojars " jar-file pom-path)) 47 | (dd/deploy {:installer :remote 48 | :artifact (b/resolve-path jar-file) 49 | :pom-file (b/pom-path {:class-dir jar-content 50 | :lib lib-name})}))) 51 | 52 | (comment 53 | (jar nil) 54 | (deploy nil)) -------------------------------------------------------------------------------- /test/scenari/v2/step_test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.step-test 2 | (:require [clojure.test :as t] 3 | [scenari.v2.step :refer [generate-step-fn]])) 4 | 5 | (defn- ->step [step-string] 6 | (let [[_spec 7 | [_narrative] 8 | [_scenarios 9 | [_scenario 10 | [_scenario_sentence] 11 | [_steps step]]]] 12 | (scenari.v2.parser/gherkin (format "Feature: \n Scenario: \n %s" step-string))] 13 | (scenari.v2.core/step->map step))) 14 | 15 | (t/deftest generate-step-fn-test 16 | (t/is (= (generate-step-fn (->step "When I create a new product with name \"iphone 6\"")) 17 | "(defwhen \"I create a new product with name {string}\" [state arg0] (do \"something\"))")) 18 | (t/is (= (generate-step-fn (->step "When I create a new product with name \"iphone 6\" and description \"awesome phone\"")) 19 | "(defwhen \"I create a new product with name {string} and description {string}\" [state arg0 arg1] (do \"something\"))")) 20 | (t/is (= (generate-step-fn (->step "When I create a new products 21 | | size | weight | 22 | | 6 | 2 |")) 23 | "(defwhen \"I create a new products\" [state arg0] (do \"something\"))")) 24 | (t/is (= (generate-step-fn (->step "When I create a new products 25 | \"\"\" 26 | this is markdown 27 | \"\"\"")) 28 | "(defwhen \"I create a new products\" [state arg0] (do \"something\"))")) 29 | (t/is (= (generate-step-fn (->step "When I create a new product with name \"iPhone 6\" and others 30 | | product name | product desc | 31 | | iPhone 7 | telephone |")) 32 | "(defwhen \"I create a new product with name {string} and others\" [state arg0 arg1] (do \"something\"))")) 33 | (t/is (= (generate-step-fn (->step "When I create a new product with id 1234 34 | | product name | product desc | 35 | | iPhone 7 | telephone |")) 36 | "(defwhen \"I create a new product with id {number}\" [state arg0 arg1] (do \"something\"))"))) 37 | 38 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["resources" "src"] 2 | 3 | :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"} 4 | "clojars" {:url "https://repo.clojars.org/"}} 5 | 6 | :deps {org.clojure/clojure {:mvn/version "1.10.2"} 7 | org.clojure/tools.logging {:mvn/version "0.4.1"} 8 | clojure.java-time/clojure.java-time {:mvn/version "0.3.2"} 9 | instaparse/instaparse {:mvn/version "1.4.12"} 10 | commons-io/commons-io {:mvn/version "2.6"} 11 | lambdaisland/kaocha {:mvn/version "1.87.1366"} 12 | org.clojure/tools.namespace {:mvn/version "0.3.1"} 13 | } 14 | 15 | :aliases {:artifact-name {:extra-deps {metav/metav {:git/url "https://github.com/jgrodziski/metav/" 16 | :sha "83dbd1fba42e868783a93c1e58b2a4d3c2a5055b"}} 17 | :main-opts ["-m" "metav.display"]} 18 | :release {:extra-deps {metav/metav {:git/url "https://github.com/jgrodziski/metav/" 19 | :sha "83dbd1fba42e868783a93c1e58b2a4d3c2a5055b"}} 20 | :main-opts ["-m" "metav.release"]} 21 | :dev {} 22 | :build {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.9.4" :git/sha "76b78fe"}} 23 | :extra-paths ["script"] 24 | :ns-default build} 25 | 26 | :deploy {:extra-deps {slipset/deps-deploy {:mvn/version "0.2.2"} 27 | io.github.clojure/tools.build {:git/tag "v0.9.4" :git/sha "76b78fe"}} 28 | :extra-paths ["script"] 29 | :ns-default build} 30 | 31 | :test {:extra-paths ["test"] 32 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"} 33 | metosin/testit {:mvn/version "0.4.0"} 34 | lambdaisland/kaocha {:mvn/version "1.0.732"}}} 35 | :runner {:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner" 36 | :sha "76568540e7f40268ad2b646110f237a60295fa3c"}} 37 | :main-opts ["-m" "cognitect.test-runner" "-d" "test"]}}} 38 | -------------------------------------------------------------------------------- /src/scenari/v2/glue.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.glue 2 | (:require [clojure.test :as t] 3 | [clojure.string :as string])) 4 | 5 | (defn all-glues 6 | "Find all glue functions (step definitions) in all loaded namespaces" 7 | [] 8 | (->> (all-ns) 9 | (mapcat #(vals (ns-publics %))) 10 | (map #(assoc (meta %) :ref %)) 11 | (filter #(contains? % :step)))) 12 | 13 | (defn ns-proximity-score 14 | "Calculate proximity score between two namespaces based on common segments" 15 | [ns-glue ns-feature] 16 | (loop [[ns-glue & child-ns-glue] (string/split ns-glue #"\.") 17 | [ns-feature & child-ns-feature] (string/split ns-feature #"\.") 18 | score 0] 19 | (if (or (not ns-feature) (not ns-glue) (not= ns-glue ns-feature)) 20 | score 21 | (recur child-ns-glue child-ns-feature (inc score))))) 22 | 23 | (defn find-closest-glues-by-ns 24 | "Find glues with closest namespace proximity to the feature namespace" 25 | [matched-glues ns-feature] 26 | (let [[_score closest-glues-by-ns] (->> matched-glues 27 | (map #(hash-map (ns-proximity-score (str ns-feature) (str (:ns %))) [%])) 28 | (apply merge-with into) 29 | (apply max-key key))] 30 | closest-glues-by-ns)) 31 | 32 | (defn- sentence-with-tokens->regex 33 | "Replace all value token like {string} and {number} in sentence. Returns a regex" 34 | [s] 35 | (-> s 36 | (string/replace #"\{string\}" "\"([^\"]*)\"") 37 | (string/replace #"\{number\}" "(\\\\d+)") 38 | re-pattern)) 39 | 40 | (defn find-glue-by-step-regex 41 | "Return the tuple of fn/regex as a vector that match the step-sentence" 42 | ([step ns-feature] (find-glue-by-step-regex step ns-feature (all-glues))) 43 | ([step ns-feature glues] 44 | (let [{:keys [sentence]} step 45 | matched-glues (filter #(seq (re-matches (sentence-with-tokens->regex (:step %)) sentence)) glues)] 46 | (cond 47 | (empty? matched-glues) 48 | (do (t/do-report {:type :missing-step, :step-sentence step}) 49 | nil) 50 | 51 | (> (count matched-glues) 1) 52 | (let [[matched-glue & conflicts] (find-closest-glues-by-ns matched-glues ns-feature)] 53 | (if conflicts 54 | (throw (RuntimeException. 55 | (str (+ (count conflicts) 1) 56 | " matching functions were found for the following step sentence:\n " 57 | sentence 58 | ", please refine your regexes that match: \n" 59 | matched-glue "\n" 60 | (string/join "\n" conflicts)))) 61 | (assoc matched-glue 62 | :warning (str (count matched-glues) " matching functions were found for this step sentence")))) 63 | 64 | :else (first matched-glues))))) -------------------------------------------------------------------------------- /src/scenari/utils.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.utils) 2 | 3 | (defn contextual-eval [ctx expr] 4 | (eval 5 | `(let [~@(mapcat (fn [[k v]] [k `'~v]) ctx)] 6 | ~expr))) 7 | 8 | (defmacro local-context [] 9 | (let [symbols (keys &env)] 10 | (zipmap (map (fn [sym] `(quote ~sym)) symbols) symbols))) 11 | 12 | (defn readr [prompt exit-code] 13 | (let [input (clojure.main/repl-read prompt exit-code)] 14 | (if (= input ::tl) 15 | exit-code 16 | input))) 17 | 18 | (defmacro break-with-repl [] 19 | `(clojure.main/repl 20 | :prompt #(print "debug=> ") 21 | :read readr 22 | :eval (partial contextual-eval (local-context)))) 23 | 24 | (defn get-whole-in 25 | "" 26 | ([tree ks] 27 | (get-whole-in tree ks nil)) 28 | ([tree ks default] 29 | (if (or (empty? ks) (nil? ks)) 30 | (if (empty? tree) 31 | default 32 | tree) 33 | (if (keyword? (first tree)) 34 | ;;mono node root tree 35 | (if (= (first tree) (first ks)) 36 | (if (empty? (rest ks)) 37 | tree 38 | (recur (rest tree) (rest ks) default)) 39 | default) 40 | ;;multi nodes root tree 41 | (recur (mapcat vector 42 | (filter (fn [node] 43 | (if (keyword? (first node)) 44 | (= (first node) (first ks)) 45 | false)) tree)) 46 | (rest ks) 47 | default))))) 48 | 49 | (defn get-in-tree 50 | "" 51 | ([tree ks] 52 | (get-in-tree tree ks nil)) 53 | ([tree ks default] 54 | (if (or (empty? ks) (nil? ks)) 55 | (if (empty? tree) 56 | default 57 | tree) 58 | (if (keyword? (first tree)) 59 | ;;mono node root tree 60 | (if (= (first tree) (first ks)) 61 | (recur (rest tree) (rest ks) default) 62 | default) 63 | ;;multi nodes root tree 64 | (recur (mapcat (fn [node] 65 | ;;remove first element and add to the 66 | (rest node)) 67 | (filter (fn [node] 68 | (if (keyword? (first node)) 69 | (= (first node) (first ks)) 70 | false)) tree)) 71 | (rest ks) 72 | default))))) 73 | 74 | (defn color-str [color & xs] 75 | (let [ansi-color #(format "\u001b[%sm" 76 | (case % :reset "0" :black "30" :red "31" 77 | :green "32" :yellow "33" :blue "34" 78 | :purple "35" :cyan "36" :white "37" :grey "90" 79 | "0"))] 80 | (str (ansi-color color) (apply str xs) (ansi-color :reset)))) 81 | 82 | (def digits-only? (re-pattern #"^[0-9.]*$")) 83 | 84 | (defn number-value-of 85 | "given a string, detect if it contains digits only, then convert to a long or Double, otherwise return the string unchanged" 86 | [s] 87 | ;;first a regex to detect if the string contains digits only 88 | (let [re-number? (re-find digits-only? s)] 89 | ;;then try to cast the string to a number going from the largest type to the shortest one 90 | (if re-number? 91 | (try 92 | (Long/valueOf s) 93 | (catch java.lang.NumberFormatException nfe 94 | (try 95 | (Double/valueOf s) 96 | (catch java.lang.NumberFormatException nfe 97 | s)))) 98 | s))) 99 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 4.0.0 7 | 8 | jar 9 | 10 | io.defsquare 11 | 12 | scenari 13 | 14 | 2.0.2-alpha 15 | 16 | scenari 17 | 18 | https://github.com/defsquare/scenari 19 | 20 | Clojure BDD library - Executable Specification with Behavior-Driven Development 21 | 22 | 23 | 24 | The MIT License (MIT) 25 | http://opensource.org/licenses/MIT 26 | repo 27 | 28 | 29 | 30 | 31 | scm:git:git://github.com:defsquare/scenari.git 32 | scm:git:ssh://github.com:defsquare/scenari.git 33 | https://github.com/defsquare/scenari 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.clojure 41 | 42 | clojure 43 | 44 | 1.11.2 45 | 46 | 47 | 48 | 49 | 50 | org.clojure 51 | 52 | tools.logging 53 | 54 | 0.4.1 55 | 56 | 57 | 58 | 59 | 60 | clojure.java-time 61 | 62 | clojure.java-time 63 | 64 | 0.3.2 65 | 66 | 67 | 68 | 69 | 70 | instaparse 71 | 72 | instaparse 73 | 74 | 1.4.10 75 | 76 | 77 | 78 | 79 | 80 | commons-io 81 | 82 | commons-io 83 | 84 | 2.14.0 85 | 86 | 87 | 88 | 89 | 90 | lambdaisland 91 | 92 | kaocha 93 | 94 | 0.0-389 95 | 96 | 97 | 98 | 99 | 100 | org.clojure 101 | 102 | tools.namespace 103 | 104 | 0.3.1 105 | 106 | 107 | 108 | 109 | 110 | 111 | src 112 | 113 | 114 | 115 | 116 | clojars 117 | https://repo.clojars.org/ 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | clojars 126 | 127 | https://repo.clojars.org/ 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /test/scenari/v2/feature_test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.feature-test 2 | (:require [clojure.test :as t :refer [deftest testing is]] 3 | [scenari.v2.core :as v2] 4 | [scenari.v2.test :as sc-test] 5 | [kaocha.type.scenari] 6 | [scenari.v2.some-glue-ns] 7 | [kaocha.repl :as krepl] 8 | [testit.core :refer :all])) 9 | 10 | (def side-effect-atom (atom 0)) 11 | (def scenario-side-effect-atom (atom 0)) 12 | 13 | (v2/defwhen #"I foo" [state] 14 | (let [scenario-side-effect @scenario-side-effect-atom 15 | side-effect-atom @side-effect-atom] 16 | (fact 1 => scenario-side-effect) 17 | (fact 1 => side-effect-atom) 18 | state)) 19 | 20 | (v2/defgiven "a doc string" [state doc-string] (is (= "This is markdown" doc-string)) state) 21 | 22 | (defn init-side-effect [] (reset! side-effect-atom 1)) 23 | (defn pre-scenario-run-side-effect [] (reset! scenario-side-effect-atom 1)) 24 | (defn post-scenario-run-side-effect [] (reset! scenario-side-effect-atom 1)) 25 | 26 | (v2/deffeature my-feature "test/scenari/v2/example.feature" 27 | {:pre-run [#'init-side-effect] 28 | :pre-scenario-run [#'pre-scenario-run-side-effect] 29 | :post-scenario-run [#'post-scenario-run-side-effect] 30 | :post-run [#'init-side-effect]}) 31 | 32 | (v2/defthen "My initial state contains foo" [state] (is (= state {:foo 1})) state) 33 | 34 | (v2/deffeature short-feature 35 | "Feature: feature description 36 | Scenario: Scenario description 37 | Then My initial state contains foo" 38 | {:default-scenario-state {:foo 1}}) 39 | 40 | (deftest scenari-runner-test 41 | (testing "Using scenari runner" 42 | (testing "execute success feature" 43 | (let [[feature-result] (v2/run-features #'scenari.v2.feature-test/short-feature)] 44 | (fact "return an execution tree with status :success" 45 | feature-result =in=> {:feature "feature description", 46 | :scenarios [{ 47 | ;:id "3e2b3c21-2b6c-407a-90f7-f10b8f16e91e", 48 | :pre-run [], 49 | :post-run [], 50 | :default-state {:foo 1}, 51 | :scenario-name " Scenario description", 52 | :steps [{:sentence-keyword :then, 53 | :input-state {:foo 1}, 54 | :raw "Then My initial state contains foo", 55 | :sentence "My initial state contains foo", 56 | :params [], 57 | :output-state {:foo 1}, 58 | :status :success, 59 | :order 0}], 60 | :status :success}], 61 | :pre-run [], 62 | :status :success}))))) 63 | 64 | (comment 65 | (remove-ns 'scenari.v2.feature-test) 66 | (meta #'scenari.v2.feature-test/my-feature) 67 | (v2/run-features) 68 | (v2/run-features #'scenari.v2.feature-test/my-feature) 69 | (sc-test/run-features #'scenari.v2.feature-test/my-feature) 70 | (krepl/test-plan) 71 | (krepl/run-all) 72 | (krepl/run :scenario)) -------------------------------------------------------------------------------- /doc/feature-structure.md: -------------------------------------------------------------------------------- 1 | # Scenari Feature Data Structure Documentation 2 | 3 | This document describes the internal data structure of a Scenari feature after parsing from Gherkin text. Understanding this structure is helpful when extending or customizing Scenari. 4 | 5 | ## Top-Level Structure 6 | 7 | A feature is represented as a map with the following keys: 8 | 9 | ```clojure 10 | {:scenarios [...] ; Vector of scenario maps 11 | :feature [...] ; Optional narrative elements 12 | :annotations #{...} ; Optional annotations (tags) 13 | :pre-run [...] ; Hook functions to execute before feature 14 | :status :success/:fail ; Status after execution 15 | } 16 | ``` 17 | 18 | ## Narrative/Feature Section 19 | 20 | The `:feature` key contains details about the narrative section: 21 | 22 | ```clojure 23 | {:feature ["Feature title" 24 | [:as_a "role"] 25 | [:I_want_to "goal"] 26 | [:so_that "benefit"]]} 27 | ``` 28 | 29 | ## Annotations 30 | 31 | Annotations (tags) are stored as a set of strings: 32 | 33 | ```clojure 34 | {:annotations #{"smoke" "regression" "api"}} 35 | ``` 36 | 37 | ## Scenarios 38 | 39 | Each scenario is represented as a map within the `:scenarios` vector: 40 | 41 | ```clojure 42 | {:id "uuid-string" ; Unique identifier 43 | :scenario-name "Name" ; The scenario title 44 | :steps [...] ; Vector of step maps 45 | :pre-run [...] ; Functions to run before scenario 46 | :post-run [...] ; Functions to run after scenario 47 | :default-state {} ; Initial state for the scenario 48 | :status :success/:fail/:pending ; Execution status 49 | } 50 | ``` 51 | 52 | ## Steps Structure 53 | 54 | Each step within a scenario is represented as a map: 55 | 56 | ```clojure 57 | {:sentence-keyword :given/:when/:then/:and ; Step type 58 | :sentence "Step text" ; The actual step text 59 | :raw "Given Step text" ; Full text with keyword 60 | :order 0 ; Position in scenario 61 | :glue {...} ; Matched step definition 62 | :params [...] ; Extracted parameters 63 | :status :success/:fail/:pending ; Execution status 64 | :input-state {} ; State before execution 65 | :output-state {} ; State after execution 66 | :exception {...} ; If step failed 67 | } 68 | ``` 69 | 70 | ## Parameters 71 | 72 | Parameters extracted from steps come in three types: 73 | 74 | ```clojure 75 | ;; Value parameters (extracted from step text) 76 | {:type :value, :val "some string"} 77 | {:type :value, :val 42} 78 | 79 | ;; Table parameters 80 | {:type :table, 81 | :val [{:header1 "value1", :header2 "value2"}, 82 | {:header1 "value3", :header2 "value4"}]} 83 | 84 | ;; Doc string parameters (multi-line text blocks) 85 | {:type :doc-string, 86 | :val "This is a multi-line\ntext block that can contain\nany content including markdown"} 87 | ``` 88 | 89 | ## Glue Metadata 90 | 91 | The `:glue` key contains information about the matched implementation function: 92 | 93 | ```clojure 94 | {:step "I do something {string}" ; Pattern to match 95 | :ns user.namespace ; Function namespace 96 | :name function-name ; Function name 97 | :ref #'user.namespace/function ; Reference to actual function 98 | :warning "Warning message" ; Optional warning 99 | } 100 | ``` 101 | 102 | ## Example Execution Flow 103 | 104 | 1. Feature is parsed from text using `gherkin-parser` 105 | 2. Steps are matched to implementation functions via `find-glue-by-step-regex` 106 | 3. During execution, each step receives the previous step's output state 107 | 4. Parameters from the step text are extracted and passed to the implementation 108 | 5. Function results and status are captured in the step's `:output-state` and `:status` 109 | 6. Scenario status is derived from all contained steps' statuses 110 | 7. Feature status is derived from all scenarios' statuses 111 | 112 | ## Common Transformations 113 | 114 | - From Gherkin text → AST via `gherkin-parser` 115 | - From AST → executable feature via `->feature-ast` 116 | - Feature execution via `run-feature` 117 | - Step execution via `run-step` 118 | 119 | ## Examples Section 120 | 121 | For scenarios with examples tables, each row generates a separate execution context: 122 | 123 | ```clojure 124 | {:scenario-name "Scenario with examples" 125 | :steps [...] 126 | :examples [{:header1 "value1", :header2 "value2"}, 127 | {:header1 "value3", :header2 "value4"}]} 128 | ``` 129 | 130 | This data structure provides a flexible representation that preserves all information from the original Gherkin text while supporting execution, reporting, and integration with test frameworks. -------------------------------------------------------------------------------- /src/scenari/v2/test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.test 2 | (:require [clojure.test :as t] 3 | [scenari.v2.step :refer [generate-step-fn]] 4 | [scenari.v2.core :refer [run-step]] 5 | [scenari.utils :as utils])) 6 | 7 | (def ^:dynamic *feature-succeed* nil) 8 | 9 | (defmethod t/report :begin-feature [m] (t/with-test-out 10 | (t/inc-report-counter :executed-features) 11 | (println) 12 | (println (str "________________________")) 13 | (println (str "Feature : " (:feature m))) 14 | (println))) 15 | 16 | (defmethod t/report :feature-succeed [_] (t/inc-report-counter :feature-succeed)) 17 | 18 | (defmethod t/report :end-feature [{:keys [succeed?]}] 19 | (if succeed? (t/inc-report-counter :feature-succeed) (t/inc-report-counter :feature-failed)) 20 | (t/with-test-out 21 | (println (str "________________________")) 22 | (println))) 23 | 24 | (defmethod t/report :begin-scenario [m] (t/with-test-out 25 | (t/inc-report-counter :test) 26 | (t/inc-report-counter :executed-scenarios) 27 | (println (str "Testing scenario : " (:scenario m))))) 28 | 29 | (defmethod t/report :begin-step [m] (t/with-test-out 30 | (let [{{:keys [raw] 31 | {glue-warning :warning 32 | glue-regex :step 33 | glue-ns :ns} :glue} :step} m 34 | information-str (str " " raw " " (utils/color-str :grey (str "(from " glue-ns "/\"" glue-regex "\")")))] 35 | (when (some? glue-warning) 36 | (println (utils/color-str :yellow glue-warning))) 37 | (println information-str)))) 38 | 39 | (defmethod t/report :step-succeed [_] (t/with-test-out "")) 40 | 41 | (defmethod t/report :step-failed [m] (t/with-test-out 42 | (println (utils/color-str :red "Step failed")) 43 | (some->> (:exception m) clojure.stacktrace/print-stack-trace))) 44 | 45 | (defmethod t/report :scenario-succeed [m] (t/with-test-out 46 | (t/inc-report-counter :pass) 47 | (t/inc-report-counter :scenarios-succeed) 48 | (println (utils/color-str :green (:scenario m) " succeed !")) 49 | (println))) 50 | 51 | (defmethod t/report :scenario-failed [m] (t/with-test-out 52 | (reset! *feature-succeed* false) 53 | (t/inc-report-counter :fail) 54 | (t/inc-report-counter :scenarios-failed) 55 | (println (utils/color-str :red (:ex m))))) 56 | 57 | (defmethod t/report :missing-step [{:keys [step-sentence]}] (t/with-test-out 58 | (println (utils/color-str :red "Missing step for : " (:raw step-sentence))) 59 | (println (utils/color-str :red (generate-step-fn step-sentence))))) 60 | 61 | (defn run-feature [feature] 62 | (when-let [{{:keys [feature scenarios pre-run]} :scenari/feature-ast} (meta feature)] 63 | (doseq [{pre-run-fn :ref} pre-run] 64 | (pre-run-fn)) 65 | (binding [*feature-succeed* (atom true)] 66 | (t/do-report {:type :begin-feature, :feature feature}) 67 | (doseq [scenario scenarios] 68 | (t/do-report {:type :begin-scenario, :scenario (:scenario-name scenario)}) 69 | (let [_ (doseq [{pre-run-fn :ref} (:pre-run scenario)] 70 | (pre-run-fn)) 71 | scenario-result (loop [state (:default-state scenario) 72 | [step & others] (:steps scenario)] 73 | (if-not step 74 | true 75 | (do 76 | (t/do-report {:type :begin-step, :step step}) 77 | (let [step-result (run-step step state)] 78 | (if (= (:status step-result) :fail) 79 | (do 80 | (t/do-report {:type :step-failed}) 81 | false) 82 | (do 83 | (t/do-report {:type :step-succeed, :state (:output-state step-result)}) 84 | (recur (:output-state step-result) others))))))) 85 | _ (doseq [{post-run-fn :ref} (:post-run scenario)] 86 | (post-run-fn))] 87 | (if scenario-result 88 | (t/do-report {:type :scenario-succeed, :scenario (:scenario-name scenario)}) 89 | (t/do-report {:type :scenario-failed 90 | :scenario (:scenario-name scenario)})))) 91 | (t/do-report {:type :end-feature, :feature feature :succeed? @*feature-succeed*})))) 92 | 93 | (defn run-features 94 | ([] (apply run-features (filter #(some? (:scenari/feature-ast (meta %))) (vals (ns-interns *ns*))))) 95 | ([& features] 96 | (doseq [feature features] 97 | (run-feature feature)))) -------------------------------------------------------------------------------- /src/kaocha/type/scenari.clj: -------------------------------------------------------------------------------- 1 | (ns kaocha.type.scenari 2 | (:require [clojure.string :as string] 3 | [clojure.test :as t] 4 | [clojure.string :as str] 5 | [clojure.java.io :as io] 6 | [clojure.spec.alpha :as s] 7 | [clojure.tools.namespace.find :as ns-find] 8 | [kaocha.testable :as testable] 9 | [kaocha.hierarchy :as hierarchy] 10 | [kaocha.repl :as krepl] 11 | [scenari.v2.core :as v2] 12 | [scenari.v2.core :as sc] 13 | [scenari.v2.test])) 14 | 15 | (s/def :kaocha.type/scenari (s/keys :req [:kaocha/source-paths 16 | :kaocha/test-paths])) 17 | 18 | (defn path->file "Looking path from resource or a file in file system" [path] 19 | (or (io/file (io/resource path)) 20 | (io/file path))) 21 | 22 | (defn find-features-meta-in-dir [path] 23 | (->> path 24 | path->file 25 | ns-find/find-namespaces-in-dir 26 | (map #(ns-publics (symbol %))) 27 | (mapcat #(map meta (vals %))) 28 | (filter #(:scenari/raw-feature %)))) 29 | 30 | (defn path->id [path] 31 | (-> path 32 | (str/replace #"/" ".") 33 | (str/replace #"_" "-") 34 | (str/replace #" " "_") 35 | (str/replace #"\.feature$" ""))) 36 | 37 | (defn ->id [s] 38 | (-> s 39 | str/trim 40 | (str/replace #"/" ".") 41 | (str/replace #"_" "-") 42 | (str/replace #" " "-"))) 43 | 44 | (defn scenario->id [scenario] 45 | (-> (:scenario-name scenario) 46 | str/trim 47 | (str/replace #" " "-"))) 48 | 49 | (defn scenario->testable [document scenario] 50 | (merge scenario 51 | {::testable/type :kaocha.type/scenari-scenario 52 | ::testable/id (keyword (scenario->id scenario)) 53 | ::testable/desc (or (:scenario-name scenario) "") 54 | ::feature (keyword (path->id (str (:project-directory document) (:file document)))) 55 | ::file (str (:project-directory document) (:file document)) 56 | })) 57 | 58 | (defn- require-all-ns [paths] 59 | (->> paths 60 | (map path->file) 61 | (mapcat ns-find/find-namespaces-in-dir) 62 | (apply require))) 63 | 64 | (defmethod testable/-load :kaocha.type/scenari [testable] 65 | (require-all-ns (::glue-paths testable)) 66 | (let [tests (for [test-path (:kaocha/test-paths testable) 67 | {{:keys [feature scenarios pre-run]} :scenari/feature-ast 68 | feature-content :scenari/raw-feature 69 | :as feature-meta} (find-features-meta-in-dir test-path)] 70 | {::testable/type :kaocha.type/scenari-feature 71 | ::testable/id (keyword (str (:ns feature-meta)) (str (:name feature-meta))) 72 | ::testable/desc feature 73 | :kaocha.test-plan/tests (mapv #(scenario->testable feature-content %) scenarios) 74 | ::pre-run pre-run})] 75 | (assoc testable :kaocha.test-plan/tests tests))) 76 | 77 | (defmethod testable/-run :kaocha.type/scenari [testable test-plan] 78 | (let [results (testable/run-testables (:kaocha.test-plan/tests testable) test-plan) 79 | testable (-> testable 80 | (dissoc :kaocha.test-plan/tests) 81 | (assoc :kaocha.result/tests results))] 82 | testable)) 83 | 84 | (defmethod testable/-run :kaocha.type/scenari-feature [testable test-plan] 85 | (t/do-report {:type :begin-feature :feature (:kaocha.testable/desc testable)}) 86 | (doseq [{pre-run-fn :ref} (::pre-run testable)] 87 | (pre-run-fn)) 88 | (let [results (testable/run-testables (:kaocha.test-plan/tests testable) test-plan) 89 | testable (-> testable 90 | (dissoc :kaocha.test-plan/tests) 91 | (assoc :kaocha.result/tests results))] 92 | (t/do-report {:type :end-feature}) 93 | testable)) 94 | 95 | (defmethod testable/-run :kaocha.type/scenari-scenario [testable test-plan] 96 | (t/do-report {:type :begin-scenario :scenario-name (:scenario-name testable)}) 97 | (let [testable (sc/run-scenario testable)] 98 | (doseq [step (:steps testable)] 99 | (condp = (:status step) 100 | :success (t/do-report {:type :begin-step :step step}) 101 | :fail (do 102 | (t/do-report {:type :begin-step :step step}) 103 | (t/do-report {:type :step-failed :exception (:exception step)})) 104 | :pending nil 105 | nil)) 106 | (-> testable 107 | (merge {:kaocha.result/count 1 108 | :kaocha.result/pass (if (= (:status testable) :success) 1 0) 109 | :kaocha.result/fail (if (= (:status testable) :fail) 1 0)})))) 110 | 111 | (defmethod testable/-run :kaocha.type/scenari-step [testable test-plan] 112 | (let [results [(v2/run-step {} testable)] 113 | testable (-> testable 114 | (dissoc :kaocha.test-plan/tests) 115 | (assoc :kaocha.result/pass results))] 116 | testable)) 117 | 118 | (s/def ::glue-paths (s/coll-of string?)) 119 | 120 | (s/def :kaocha.type/scenari (s/keys :req [:kaocha/source-paths 121 | :kaocha/test-paths 122 | ::glue-paths])) 123 | 124 | (s/def :kaocha.type/scenari-feature any?) 125 | (s/def :kaocha.type/scenari-scenario any?) 126 | (s/def :kaocha.type/scenari-step any?) 127 | 128 | 129 | 130 | (hierarchy/derive! ::begin-feature :kaocha/begin-group) 131 | (hierarchy/derive! ::end-feature :kaocha/end-group) 132 | 133 | (hierarchy/derive! ::begin-scenario :kaocha/begin-test) 134 | (hierarchy/derive! ::end-scenario :kaocha/end-test) 135 | 136 | (hierarchy/derive! :kaocha.type/scenari :kaocha.testable.type/suite) 137 | (hierarchy/derive! :kaocha.type/scenari-feature :kaocha.testable.type/group) 138 | (hierarchy/derive! :kaocha.type/scenari-scenario :kaocha.testable.type/leaf) 139 | 140 | 141 | (comment 142 | (in-ns 'kaocha.type.scenari) 143 | (krepl/run :scenario) 144 | (krepl/run :unit) 145 | 146 | (krepl/run {:config-file "tests.edn"}) 147 | 148 | (krepl/test-plan) 149 | 150 | (krepl/test-plan {:tests [{:id :scenario 151 | :type :kaocha.type/scenari 152 | :kaocha/source-paths ["src"] 153 | :kaocha/test-paths ["test/scenari/v2"] 154 | :scenari.v2.kaocha/glue-paths ["test/scenari/v2"]}]})) 155 | -------------------------------------------------------------------------------- /test/scenari/v2/glue_test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.glue-test 2 | (:require [clojure.test :refer :all] 3 | [scenari.v2.glue :as glue])) 4 | 5 | ;; ns-proximity-score tests 6 | (deftest ns-proximity-score-test 7 | (testing "Calculates proximity score between namespaces" 8 | (is (= 0 (glue/ns-proximity-score "some.ns" "other.ns"))) 9 | (is (= 1 (glue/ns-proximity-score "some.ns" "some.other"))) 10 | (is (= 2 (glue/ns-proximity-score "some.ns.path" "some.ns.other"))) 11 | (is (= 3 (glue/ns-proximity-score "some.ns.path.one" "some.ns.path.two"))) 12 | (is (= 0 (glue/ns-proximity-score "some" "other"))) 13 | (is (= 0 (glue/ns-proximity-score "" "some"))) 14 | (is (= 0 (glue/ns-proximity-score "some" ""))) 15 | (is (= 1 (glue/ns-proximity-score "" ""))) 16 | (is (= 1 (glue/ns-proximity-score "scenari" "scenari"))) 17 | (is (= 3 (glue/ns-proximity-score "scenari.v2.test" "scenari.v2.test"))))) 18 | 19 | ;; find-closest-glues-by-ns tests 20 | (deftest find-closest-glues-by-ns-test 21 | (testing "Finds closest glues based on namespace proximity" 22 | (let [matched-glues [{:ns 'some.ns.far :name 'test-step} 23 | {:ns 'some.ns :name 'other-step} 24 | {:ns 'other.ns :name 'another-step}]] 25 | (is (= 2 (count (glue/find-closest-glues-by-ns matched-glues 'some.ns.feature)))) 26 | (is (every? #(contains? #{'some.ns.far 'some.ns} (:ns %)) 27 | (glue/find-closest-glues-by-ns matched-glues 'some.ns.feature)))))) 28 | 29 | (deftest multiple-glues-same-score-test 30 | (testing "When there are multiple glues with same proximity score" 31 | (let [matched-glues [{:ns 'scenari.v2.glue :name 'test-step} 32 | {:ns 'scenari.v2.other :name 'other-step} 33 | {:ns 'other.ns :name 'another-step}]] 34 | (is (= 2 (count (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.test)))) 35 | (is (every? #(contains? #{'scenari.v2.glue 'scenari.v2.other} (:ns %)) 36 | (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.test)))))) 37 | 38 | (deftest real-world-namespace-test 39 | (testing "With real-world namespace examples" 40 | (let [matched-glues [{:ns 'scenari.v2.glue :name 'test-step} 41 | {:ns 'scenari.v2.other-glue.glue :name 'other-step} 42 | {:ns 'scenari.other :name 'another-step}]] 43 | ;; Same namespace matches exactly 44 | (is (= 1 (count (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.glue)))) 45 | (is (= 'scenari.v2.glue (:ns (first (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.glue))))) 46 | 47 | ;; Namespaces at same depth but different names 48 | (is (= 2 (count (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.feature)))) 49 | (is (every? #(contains? #{'scenari.v2.glue 'scenari.v2.other-glue.glue} (:ns %)) 50 | (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.feature))) 51 | 52 | ;; Feature namespace is deeper than glue namespaces 53 | (is (= 2 (count (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.feature.test)))) 54 | (is (every? #(contains? #{'scenari.v2.glue 'scenari.v2.other-glue.glue} (:ns %)) 55 | (glue/find-closest-glues-by-ns matched-glues 'scenari.v2.feature.test)))))) 56 | 57 | ;; find-glue-by-step-regex tests 58 | (deftest find-glue-by-step-regex-test 59 | (testing "Finding glue with exact match" 60 | (let [step {:sentence "I do something special"} 61 | ns-feature 'test.ns 62 | glues [{:step "I do something special" 63 | :ns 'test.ns 64 | :name 'exact-match-fn}]] 65 | (is (= 'exact-match-fn (:name (glue/find-glue-by-step-regex step ns-feature glues)))) 66 | (is (= 'test.ns (:ns (glue/find-glue-by-step-regex step ns-feature glues)))))) 67 | 68 | (testing "Finding glue with regex pattern" 69 | (let [step {:sentence "I count 42 items"} 70 | ns-feature 'test.ns 71 | glues [{:step "I count {number} items" 72 | :ns 'test.ns 73 | :name 'matching-fn}]] 74 | (is (= 'matching-fn (:name (glue/find-glue-by-step-regex step ns-feature glues)))))) 75 | 76 | (testing "Finding glue with string pattern" 77 | (let [step {:sentence "I use \"test-value\" as parameter"} 78 | ns-feature 'test.ns 79 | glues [{:step "I use {string} as parameter" 80 | :ns 'test.ns 81 | :name 'string-param-fn}]] 82 | (is (= 'string-param-fn (:name (glue/find-glue-by-step-regex step ns-feature glues)))))) 83 | 84 | (testing "Multiple matching glues with different namespace proximity" 85 | (let [step {:sentence "I do common action"} 86 | ns-feature 'test.ns.feature 87 | glues [{:step "I do common action" 88 | :ns 'other.ns 89 | :name 'first-fn} 90 | {:step "I do common action" 91 | :ns 'test.ns.glue 92 | :name 'second-fn}]] 93 | ;; It should prefer the glue with namespace closer to feature namespace 94 | (is (= 'second-fn (:name (glue/find-glue-by-step-regex step ns-feature glues)))) 95 | (is (= 'test.ns.glue (:ns (glue/find-glue-by-step-regex step ns-feature glues)))))) 96 | 97 | (testing "With token replacement in sentence" 98 | (let [step {:sentence "The value is 123"} 99 | ns-feature 'test.ns 100 | glues [{:step "The value is {number}" 101 | :ns 'test.ns 102 | :name 'number-fn}]] 103 | (is (= 'number-fn (:name (glue/find-glue-by-step-regex step ns-feature glues))))))) 104 | 105 | (deftest find-glue-with-conflict-test 106 | (testing "With exact conflict in different namespaces - should throw exception" 107 | (let [step {:sentence "Duplicate in different namespaces"} 108 | ns-feature 'test.ns.feature 109 | glues [{:step "Duplicate in different namespaces" 110 | :ns 'test.ns.one 111 | :name 'first-fn} 112 | {:step "Duplicate in different namespaces" 113 | :ns 'test.ns.two 114 | :name 'second-fn}]] 115 | ;; Both namespaces have the same proximity score to feature, should throw 116 | (is (thrown-with-msg? 117 | RuntimeException 118 | #"2 matching functions were found for the following step sentence" 119 | (with-redefs [clojure.test/do-report (fn [_m] nil)] 120 | (glue/find-glue-by-step-regex step ns-feature glues))))))) 121 | 122 | (deftest simple-missing-glue-test 123 | (testing "No matching glue returns nil" 124 | (let [step {:sentence "This has no matching glue"} 125 | ns-feature 'test.ns 126 | glues []] 127 | (is (nil? (with-redefs [clojure.test/do-report (fn [_m] nil)] 128 | (glue/find-glue-by-step-regex step ns-feature glues))))))) 129 | 130 | 131 | (comment 132 | (run-tests)) -------------------------------------------------------------------------------- /src/scenari/v2/parser.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.parser 2 | (:require [instaparse.core :as insta])) 3 | 4 | (def kw-translations-data {:fr {:given "Etant donné que " :when "Quand " :and "Et " 5 | :then "Alors " :scenario "Scénario :" 6 | :examples "Exemples :" 7 | :narrative "Narrative: " 8 | :as_a "En tant que " 9 | :in_order_to " afin de " 10 | :I_want_to " Je veux " 11 | :so_that " afin de "} 12 | :en {:given "Given " :when "When " :and "And " 13 | :then "Then " :scenario "Scenario:" 14 | :examples "Examples:" 15 | :narrative "Narrative: " 16 | :as_a "As a " 17 | :in_order_to " in order to " 18 | :I_want_to " I want to " 19 | :so_that " so that "}}) 20 | 21 | (defn- kw-translations 22 | "return a string consisting of appending the keyword separated by | for inclusion in gherkin grammar" 23 | ([kw data] 24 | (apply str 25 | (interpose "|" 26 | (map (comp #(str "'" % "'") 27 | val 28 | first 29 | (partial filter (fn [e] (= (key e) kw)))) 30 | (vals data))))) 31 | ([kw] 32 | (kw-translations kw kw-translations-data))) 33 | 34 | (def gherkin (insta/parser 35 | (str " 36 | SPEC = annotations? narrative? scenarios 37 | narrative = <'Narrative: '|'Feature: '> #'.*' ? (as_a I_want_to in_order_to | 38 | as_a I_want_to so_that | in_order_to as_a I_want_to | 39 | as_a in_order_to I_want_to)? 40 | annotations = ( annotation )* 41 | annotation = <'@'> #'\\w+' 42 | in_order_to = ? <'In order to '> #'.*' 43 | as_a = ? <'As a '> #'.*' 44 | I_want_to = ? <'I want to '> #'.*' 45 | so_that = ? <'So that '> #'.*' 46 | scenarios = (scenario )* 47 | = " (kw-translations :scenario) " 48 | scenario = scenario_sentence steps examples? 49 | = (comment_line whitespace?)* 50 | = <'#'> 51 | steps = (comment | | step_sentence | )* 52 | given = <" (kw-translations :given) "> 53 | when = <" (kw-translations :when) "> 54 | then = <" (kw-translations :then) "> 55 | and = <" (kw-translations :and) "> 56 | = given | when | then | and 57 | = #'\\s+' 58 | = ' ' | '\t' 59 | = #'\r?\n' 60 | scenario_sentence = #'.*' 61 | step_sentence = step_keywords sentence ( (tab_params | doc_string))? 62 | sentence = #'.*' 63 | doc_string = <'\"\"\"'> doc_content <'\"\"\"'> 64 | doc_content = #'(?:[^\"]+|\"(?!\"\"))*' 65 | examples = examples-keywords header row* 66 | = <" (kw-translations :examples) "> 67 | tab_params = header row* 68 | header = (<'|'> column_name)+ <'|'> 69 | = #'[^|]*' 70 | row = (<'|'> value )+ <'|'> 71 | = #'[^|]*' 72 | word = #'[\\p{L}$€]+' 73 | number = #'[0-9]+' 74 | "))) 75 | 76 | (def sentence (insta/parser 77 | (str "SENTENCE = ? (words | data_group | parameter)* ? 78 | words = #'[a-zA-Z./\\_\\-\\'èéêàûù ]+' 79 | = #'[a-zA-Z\"./\\_\\- ]+' 80 | parameter = <'<'> parameter_name <'>'> | <'${'> parameter_name <'}'> 81 | string = <'\"'> #'[^\"]*' <'\"'> 82 | number = #'\\d+' 83 | = string | number | map | vector 84 | map = #'\\{[a-zA-Z0-9\\-:,./\\\" ]+\\}' 85 | elements = (#'\".+\"|[0-9]+' ?)* 86 | vector = <'['> elements <']'> 87 | = #'\\s+' 88 | = #'[a-zA-Z0-9+ ]*' 89 | whitespace = #'\\s+' 90 | eol = #'\r?\n'"))) 91 | 92 | (def step (insta/parser 93 | (str "STEP = step_keyword (words | data_group | parameter)* ? 94 | given = <" (kw-translations :given) "> 95 | when = <" (kw-translations :when) "> 96 | then = <" (kw-translations :then) "> 97 | and = <" (kw-translations :and) "> 98 | words = #'[a-zA-Z./\\_\\-\\'èéêàûù ]+' 99 | = #'[a-zA-Z\"./\\_\\- ]+' 100 | parameter = <'<'> parameter_name <'>'> | <'${'> parameter_name <'}'> 101 | string = <'\"'> #'[^\"]*' <'\"'> 102 | number = #'\\d+' 103 | = string | number | map | vector 104 | map = #'\\{[a-zA-Z0-9\\-:,./\\\" ]+\\}' 105 | elements = (#'\".+\"|[0-9]+' ?)* 106 | vector = <'['> elements <']'> 107 | = given | when | then | and 108 | = #'\\s+' 109 | = #'[a-zA-Z0-9+ ]*' 110 | whitespace = #'\\s+' 111 | eol = #'\r?\n'"))) 112 | -------------------------------------------------------------------------------- /scenari.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scenarior 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Scenari 15 | 16 | 17 | 18 | 19 | 20 | 21 | Given 22 | 23 | 24 | 25 | 26 | 27 | When 28 | 29 | 30 | 31 | 32 | 33 | Then 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/scenari/v2/parsing_test.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.parsing-test 2 | (:require [clojure.test :refer :all] 3 | [scenari.v2.parser :refer [gherkin sentence]])) 4 | 5 | 6 | (deftest basic-feature-skeleton-test 7 | (is (= (gherkin " 8 | Feature: my feature 9 | Scenario: scenario 1 10 | Given a step 11 | When do something 12 | Then something happened 13 | 14 | Scenario: scenario 2 15 | Given a step") 16 | [:SPEC 17 | [:narrative "my feature"] 18 | [:scenarios 19 | [:scenario 20 | [:scenario_sentence " scenario 1"] 21 | [:steps 22 | [:step_sentence [:given] [:sentence "a step"]] 23 | [:step_sentence [:when] [:sentence "do something"]] 24 | [:step_sentence [:then] [:sentence "something happened"]]]] 25 | [:scenario 26 | [:scenario_sentence " scenario 2"] 27 | [:steps 28 | [:step_sentence [:given] [:sentence "a step"]]]]]] 29 | ))) 30 | 31 | (deftest feature-with-annotation-test 32 | (is (= (gherkin " 33 | @Annotation1 @Annotation2 34 | Feature: my feature 35 | Scenario: scenario 1 36 | Given a step") 37 | [:SPEC 38 | [:annotations [:annotation "Annotation1"] [:annotation "Annotation2"]] 39 | [:narrative "my feature"] 40 | [:scenarios 41 | [:scenario 42 | [:scenario_sentence " scenario 1"] 43 | [:steps [:step_sentence [:given] [:sentence "a step"]]]]]]))) 44 | 45 | (def scenario-with-examples " 46 | Scenario: create a new product 47 | # this is a comment 48 | When I create a new product with name and description 49 | Then I receive a response with an id 50 | And a location URL 51 | Examples: 52 | | product_name | product_desc | 53 | | iPhone 6 | telephone | 54 | | iPhone 6+ | bigger telephone | 55 | | iPad | tablet | 56 | ") 57 | 58 | (deftest feature-parsing-test 59 | (is (= (gherkin " 60 | Scenario: test example section 61 | Given a location URL 62 | Examples: 63 | | product_name | product_desc | 64 | | iPhone 6 | telephone | 65 | | iPhone 6+ | bigger telephone | 66 | | iPad | tablet | 67 | ") 68 | 69 | [:SPEC 70 | [:scenarios 71 | [:scenario 72 | [:scenario_sentence " test example section"] 73 | [:steps [:step_sentence [:given] [:sentence "a location URL"]]] 74 | [:examples 75 | [:header " product_name " "product_desc "] 76 | [:row " iPhone 6 " "telephone "] 77 | [:row " iPhone 6+ " "bigger telephone "] 78 | [:row " iPad " " tablet "]]]]] 79 | ))) 80 | 81 | 82 | (deftest scenario-with-examples-test 83 | (is (= (gherkin " 84 | Scenario: test example section 85 | Given a location URL 86 | Examples: 87 | | product_name | product_desc | 88 | | iPhone 6 | telephone | 89 | | iPhone 6+ | bigger telephone | 90 | | iPad | tablet | 91 | ") 92 | 93 | [:SPEC 94 | [:scenarios 95 | [:scenario 96 | [:scenario_sentence " test example section"] 97 | [:steps [:step_sentence [:given] [:sentence "a location URL"]]] 98 | [:examples 99 | [:header " product_name " "product_desc "] 100 | [:row " iPhone 6 " "telephone "] 101 | [:row " iPhone 6+ " "bigger telephone "] 102 | [:row " iPad " " tablet "]]]]] 103 | ))) 104 | 105 | (deftest scenario-with-tab-params-test 106 | (is (= (gherkin " 107 | Scenario: create a new product 108 | # this is a comment 109 | When I create a new products 110 | | product_name | product_desc | 111 | | iPhone 6 | telephone | 112 | | iPhone 6+ | bigger telephone | 113 | | iPad | tablet | 114 | Then I receive a response with an id") 115 | 116 | [:SPEC 117 | [:scenarios 118 | [:scenario 119 | [:scenario_sentence " create a new product"] 120 | [:steps 121 | [:step_sentence 122 | [:when] 123 | [:sentence "I create a new products"] 124 | [:tab_params 125 | [:header " product_name " " product_desc "] 126 | [:row " iPhone 6 " " telephone "] 127 | [:row " iPhone 6+ " " bigger telephone "] 128 | [:row " iPad " " tablet "]]] 129 | [:step_sentence [:then] [:sentence "I receive a response with an id"]]]]]]))) 130 | 131 | (deftest feature-with-narrative-test 132 | (is (= (gherkin " 133 | Feature: feature with full narrative 134 | As a user 135 | I want to login 136 | So that I gain access to the protected resource 137 | 138 | Scenario: scenario 1 139 | Given a step") 140 | [:SPEC 141 | [:narrative 142 | "feature with full narrative" 143 | [:as_a "user"] 144 | [:I_want_to "login"] 145 | [:so_that "I gain access to the protected resource"]] 146 | [:scenarios 147 | [:scenario 148 | [:scenario_sentence " scenario 1"] 149 | [:steps 150 | [:step_sentence [:given] [:sentence "a step"]]]]]]))) 151 | 152 | (deftest sentence-test 153 | (testing "Parsing sentences with parameters" 154 | (is (= (sentence "I create a new product with name \"iphone 6\" and description \"awesome phone\"") 155 | [:SENTENCE 156 | [:words "I create a new product with name "] 157 | [:string "iphone 6"] 158 | [:words " and description "] 159 | [:string "awesome phone"]])) 160 | 161 | (is (= (sentence "I buy 42 products") 162 | [:SENTENCE 163 | [:words "I buy "] 164 | [:number "42"] 165 | [:words " products"]])) 166 | 167 | (is (= (sentence "I create a new product with and price ${price}") 168 | [:SENTENCE 169 | [:words "I create a new product with "] 170 | [:parameter "product_name"] 171 | [:words " and price "] 172 | [:parameter "price"]])) 173 | 174 | (is (= (sentence "I create a product with map {\"name\":\"phone\",\"price\":499}") 175 | [:SENTENCE 176 | [:words "I create a product with map "] 177 | [:map "{\"name\":\"phone\",\"price\":499}"]])))) 178 | 179 | (deftest unicode-character-test 180 | (is (= (gherkin " 181 | Scenario: scenario with unicode characters 182 | Given a product with name \"Téléphone\" 183 | When I add to cart with price €100 184 | Then I should see \"Produit ajouté !\"") 185 | [:SPEC 186 | [:scenarios 187 | [:scenario 188 | [:scenario_sentence " scenario with unicode characters"] 189 | [:steps 190 | [:step_sentence [:given] [:sentence "a product with name \"Téléphone\""]] 191 | [:step_sentence [:when] [:sentence "I add to cart with price €100"]] 192 | [:step_sentence [:then] [:sentence "I should see \"Produit ajouté !\""]]]]]])) 193 | 194 | (is (= (sentence "a product with name \"Téléphone\"") 195 | [:SENTENCE 196 | [:words "a product with name "] 197 | [:string "Téléphone"]]))) 198 | 199 | (deftest empty-feature-test 200 | (is (= (gherkin "") 201 | [:SPEC [:scenarios]]))) 202 | 203 | (deftest commented-feature-test 204 | (is (= (gherkin " 205 | # This is a comment at the top of the file 206 | # Multi-line comment 207 | Feature: feature with comments 208 | # Comment after feature name 209 | 210 | # Comment before scenario 211 | Scenario: scenario with comments 212 | # Comment before step 213 | Given a step 214 | # Comment between steps 215 | When another step 216 | # Comment after steps 217 | 218 | # Comment at the end") 219 | [:SPEC 220 | [:narrative "feature with comments"] 221 | [:scenarios 222 | [:scenario 223 | [:scenario_sentence " scenario with comments"] 224 | [:steps 225 | [:step_sentence [:given] [:sentence "a step"]] 226 | [:step_sentence [:when] [:sentence "another step"]]]]]]))) 227 | 228 | (deftest feature-with-doc-string-test 229 | (testing "Parsing features with doc strings" 230 | (is (= (gherkin " 231 | Feature: feature with markdown 232 | Scenario: scenario with markdown 233 | Given a markdown 234 | \"\"\" 235 | This is markdown 236 | \"\"\"") 237 | [:SPEC 238 | [:narrative "feature with markdown"] 239 | [:scenarios 240 | [:scenario 241 | [:scenario_sentence " scenario with markdown"] 242 | [:steps 243 | [:step_sentence [:given] [:sentence "a markdown"] 244 | [:doc_string [:doc_content " This is markdown\n "]]]]]]])) 245 | 246 | (is (= (gherkin " 247 | Scenario: scenario with multiline doc string 248 | When I provide documentation 249 | \"\"\" 250 | Line 1 251 | Line 2 252 | Line 3 253 | \"\"\" 254 | Then it should be processed") 255 | [:SPEC 256 | [:scenarios 257 | [:scenario 258 | [:scenario_sentence " scenario with multiline doc string"] 259 | [:steps 260 | [:step_sentence [:when] [:sentence "I provide documentation"] 261 | [:doc_string [:doc_content " Line 1\n Line 2\n Line 3\n "]]] 262 | [:step_sentence [:then] [:sentence "it should be processed"]]]]]])))) -------------------------------------------------------------------------------- /src/scenari/v2/core.clj: -------------------------------------------------------------------------------- 1 | (ns scenari.v2.core 2 | (:require [clojure.test :as t] 3 | [clojure.java.io :as io] 4 | [clojure.string :as string] 5 | [instaparse.transform :as insta-trans] 6 | [scenari.v2.parser :as parser] 7 | [scenari.v2.glue :as glue]) 8 | (:import (java.io File) 9 | (org.apache.commons.io FileUtils) 10 | (java.util UUID))) 11 | 12 | 13 | ;; ------------------------ 14 | ;; LOAD 15 | ;; ------------------------ 16 | 17 | (defn tab-params->params [[param-type [_ & headers] & rows]] 18 | (when (= :tab_params param-type) 19 | (let [param-names (map (comp keyword string/trim) headers) 20 | params-values (map (comp #(map string/trim %) rest) rows)] 21 | [{:type :table :val (mapv #(apply hash-map (interleave param-names %)) params-values)}]))) 22 | 23 | (defn doc-string->params [[param-type [_ content]]] 24 | (when (= :doc_string param-type) 25 | [{:type :doc-string :val (string/trim content)}])) 26 | 27 | (defn sentence-params->params [[type val]] {:type :value :val (condp = type 28 | :number (read-string val) 29 | :string (str val))}) 30 | 31 | (defn file-from-fs-or-classpath [x] 32 | (let [r (io/resource x) 33 | f (when (and (instance? File x) (.exists x)) x) 34 | f-str (when (and (instance? String x) (.exists (io/as-file x))) x)] 35 | (io/as-file (or r f f-str)))) 36 | 37 | (defn get-feature-files [basedir] 38 | (letfn [(find-spec-files [basedir] 39 | (FileUtils/listFiles 40 | basedir 41 | (into-array ["story" "feature"]) 42 | true ;;recursive 43 | ))] 44 | (case (str (type basedir)) 45 | "class java.lang.String" (if (.exists (File. ^String basedir)) 46 | (find-spec-files (File. ^String basedir)) 47 | (throw (RuntimeException. (str basedir " doesn't exists in path: " (System/getProperty "user.dir"))))) 48 | "class java.io.File" (find-spec-files basedir)))) 49 | 50 | (defn find-sentence-params [sentence] 51 | (insta-trans/transform 52 | {:SENTENCE (fn [& s] (->> s 53 | (filter (fn [[type _]] (#{:string :number} type))) 54 | (mapv sentence-params->params)))} 55 | (parser/sentence sentence))) 56 | 57 | (defmulti read-source 58 | (fn [path] 59 | (letfn [(file-or-dir [x] 60 | (cond (.isFile x) :file 61 | (.isDirectory x) :dir))] 62 | (if (instance? String path) 63 | (if-let [f (file-from-fs-or-classpath path)] 64 | (file-or-dir f) 65 | :feature-as-str) 66 | (if (instance? File path) 67 | (file-or-dir path) 68 | (throw (RuntimeException. (str "type " (type path) "for spec not accepted (only string or file)"))))))) 69 | :default :file) 70 | 71 | (defmethod read-source 72 | :dir 73 | [path] 74 | (doseq [spec-file (get-feature-files path)] 75 | (read-source spec-file))) 76 | 77 | (defmethod read-source 78 | :file 79 | [path-or-source] 80 | (read-source (slurp (file-from-fs-or-classpath path-or-source)))) 81 | 82 | (defmethod read-source :feature-as-str [source] source) 83 | 84 | (defn step->map [[_step-sentence [step-key] [_sentence sentence] data-param]] 85 | (merge {:sentence-keyword step-key 86 | :sentence sentence 87 | :raw (str (string/capitalize (name step-key)) " " sentence)} 88 | (when-let [params (into (find-sentence-params sentence) 89 | (or (tab-params->params data-param) 90 | (doc-string->params data-param)))] 91 | {:params params}))) 92 | 93 | (defn ->feature-ast [source {:keys [pre-run pre-scenario-run post-scenario-run default-scenario-state] :as _options} ns-feature] 94 | (insta-trans/transform 95 | {:SPEC (fn [& s] (apply merge s)) 96 | :annotation (fn [s] s) 97 | :annotations (fn [& s] {:annotations (set s)}) 98 | :narrative (fn [& n] {:feature (string/join " " n)}) 99 | :steps (fn [& contents] 100 | {:steps (vec (map-indexed (fn [i content] 101 | (let [step (step->map content)] 102 | (-> step 103 | (assoc :order i) 104 | (assoc :glue (glue/find-glue-by-step-regex step ns-feature))))) 105 | contents))}) 106 | :scenario_sentence (fn [a] {:scenario-name a}) 107 | :scenario (fn [& contents] (into {:id (.toString (UUID/randomUUID)) 108 | :pre-run (map #(assoc (meta %) :ref %) pre-scenario-run) 109 | :post-run (map #(assoc (meta %) :ref %) post-scenario-run) 110 | :default-state (or default-scenario-state {})} 111 | contents)) 112 | :scenarios (fn [& contents] {:scenarios (into [] contents) 113 | :pre-run (map #(assoc (meta %) :ref %) pre-run)})} 114 | (parser/gherkin source))) 115 | 116 | ;; ------------------------ 117 | ;; RUN 118 | ;; ------------------------ 119 | 120 | (defn run-step [step scenario-state] 121 | (binding [clojure.test/*report-counters* (ref clojure.test/*initial-report-counters*)] 122 | (let [f (get-in step [:glue :ref]) 123 | params (cons scenario-state (mapv :val (get step :params)))] 124 | (try (let [result (apply f params) 125 | state (last result) 126 | any-fail? (> (:fail (deref clojure.test/*report-counters*)) 0)] 127 | (-> step 128 | (assoc :input-state scenario-state) 129 | (assoc :output-state state) 130 | (assoc :status (if any-fail? :fail :success)))) 131 | (catch Throwable e 132 | (-> step 133 | (assoc :input-state scenario-state) 134 | (assoc :exception e) 135 | (assoc :status :fail))))))) 136 | 137 | (defn run-steps [steps state [step & others]] 138 | (if-not step 139 | steps 140 | (let [{:keys [output-state status] :as step-result} (run-step step state) 141 | steps (map #(if (= (:order step-result) (:order %)) step-result %) steps)] 142 | (if (= status :fail) 143 | steps 144 | (recur steps output-state others))))) 145 | 146 | (defn run-scenario [scenario] 147 | (let [default-state (:default-state scenario) 148 | pending-steps (map #(assoc % :status :pending) (:steps scenario)) 149 | _ (doseq [{pre-run-fn :ref} (:pre-run scenario)] 150 | (pre-run-fn)) 151 | result-steps (run-steps pending-steps default-state pending-steps) 152 | _ (doseq [{post-run-fn :ref} (:post-run scenario)] 153 | (post-run-fn))] 154 | (-> scenario 155 | (assoc :steps result-steps) 156 | (assoc :status (if (contains? (set (map :status result-steps)) :fail) :fail :success))))) 157 | 158 | (defn run-scenarios [scenarios [scenario & others]] 159 | (if-not scenario 160 | scenarios 161 | (let [scenario-result (run-scenario scenario) 162 | scenarios (map #(if (= (:id %) (:id scenario)) scenario-result %) scenarios)] 163 | (recur scenarios others)))) 164 | 165 | (defn run-feature [feature] 166 | (let [{:keys [scenarios pre-run] :as feature-ast} (get (meta feature) :scenari/feature-ast)] 167 | (doseq [{pre-run-fn :ref} pre-run] 168 | (pre-run-fn)) 169 | (let [scenarios (run-scenarios scenarios scenarios)] 170 | (-> feature-ast 171 | (assoc :scenarios scenarios) 172 | (assoc :status (if (contains? (set (map :status scenarios)) :fail) :fail :success)))))) 173 | 174 | (defn run-features 175 | ([] (apply run-features (filter #(some? (:scenari/feature-ast (meta %))) (vals (ns-interns *ns*))))) 176 | ([& features] (map run-feature features))) 177 | 178 | 179 | ;; ------------------------ 180 | ;; DEFINE 181 | ;; ------------------------ 182 | (defmacro deffeature [name feature & [options]] 183 | (let [feature# `~(eval feature) 184 | name# `~(if (symbol? name) name (eval name)) 185 | source# (read-source feature#) 186 | feature-ast# `(->feature-ast ~source# ~options *ns*)] 187 | `(do 188 | (ns-unmap *ns* '~name#) 189 | (require '[scenari.v2.test]) 190 | (t/deftest ~(vary-meta name# assoc 191 | :scenari/raw-feature source# 192 | :scenari/feature-ast feature-ast# 193 | :scenari/feature-test true) [] (scenari.v2.test/run-features (var ~name#)))))) 194 | 195 | 196 | (defn re->symbol [re] 197 | (-> (str re) 198 | (string/replace #"\\\"\(\.\*\)\\\"" "param") 199 | (string/replace #" " "-") 200 | symbol)) 201 | 202 | ;; TODO make a step evaluable as a standalone fun 203 | ;; TODO duplication, should be resolve with a macro 204 | (defmacro defgiven [regex params & body] 205 | `(defn ~(-> (re->symbol regex) 206 | (vary-meta assoc :step regex)) ~params (into [] [~@body]))) 207 | 208 | (defmacro defand [regex params & body] 209 | `(defn ~(-> (re->symbol regex) 210 | (vary-meta assoc :step regex)) ~params (into [] [~@body]))) 211 | 212 | (defmacro defwhen [regex params & body] 213 | `(defn ~(-> (re->symbol regex) 214 | (vary-meta assoc :step regex)) ~params (into [] [~@body]))) 215 | 216 | (defmacro defthen [regex params & body] 217 | `(defn ~(-> (re->symbol regex) 218 | (vary-meta assoc :step regex)) ~params (into [] [~@body]))) -------------------------------------------------------------------------------- /doc/development-workflow.md: -------------------------------------------------------------------------------- 1 | # Scenari Development Workflow 2 | 3 | This document outlines the development workflow when using Scenari for Behavior-Driven Development (BDD) in Clojure projects. It covers the complete journey from writing scenarios to executing and maintaining them. 4 | 5 | ## Overview 6 | 7 | The Scenari development workflow follows these main steps: 8 | 9 | 1. Write scenarios in Gherkin format (`.feature` files) 10 | 2. Define feature references using `deffeature` 11 | 3. Implement step definitions (glue code) 12 | 4. Execute and validate the scenarios 13 | 5. Refine and iterate 14 | 15 | Let's explore each step in detail. 16 | 17 | ## 1. Writing Feature Files 18 | 19 | Feature files use the Gherkin syntax and typically have a `.feature` extension. They describe behaviors from a user's perspective. 20 | 21 | ### Basic Structure 22 | 23 | ```gherkin 24 | Feature: Shopping Cart 25 | As a customer 26 | I want to manage items in my cart 27 | So that I can purchase what I need 28 | 29 | Scenario: Add item to empty cart 30 | Given I have an empty shopping cart 31 | When I add "Clojure Programming" book to the cart 32 | Then my cart should contain 1 item 33 | And the item should be "Clojure Programming" book 34 | ``` 35 | 36 | ### Key Components 37 | 38 | - **Feature**: The overall functionality being described 39 | - **Narrative**: "As a..., I want to..., So that..." pattern explaining the purpose 40 | - **Scenarios**: Specific examples of the feature in action 41 | - **Steps**: Individual actions and assertions (Given/When/Then/And) 42 | 43 | ### Tips for Writing Good Scenarios 44 | 45 | - Focus on business value and user perspective 46 | - Keep scenarios concise and focused on a single behavior 47 | - Use declarative style ("what" rather than "how") 48 | - Maintain consistency in terminology 49 | - Use data tables for multiple examples 50 | 51 | ### Example with Data Tables 52 | 53 | ```gherkin 54 | Scenario: Calculate discounts 55 | Given the following products in catalog: 56 | | product | price | category | 57 | | Keyboard | 100 | hardware | 58 | | Mouse | 50 | hardware | 59 | | Clojure | 40 | book | 60 | When I apply the "SUMMER10" discount code 61 | Then the prices should be: 62 | | product | discounted_price | 63 | | Keyboard | 90 | 64 | | Mouse | 45 | 65 | | Clojure | 36 | 66 | ``` 67 | 68 | ### Example with Scenario Outline 69 | 70 | ```gherkin 71 | Scenario Outline: Apply tax based on location 72 | Given a product with price 73 | When shipping to 74 | Then the final price should be 75 | 76 | Examples: 77 | | base_price | location | final_price | 78 | | 100 | US | 108 | 79 | | 100 | EU | 120 | 80 | | 100 | AU | 110 | 81 | ``` 82 | 83 | ### Example with Doc Strings 84 | 85 | Doc strings (delimited by triple quotes) are useful for passing multi-line text content: 86 | 87 | ```gherkin 88 | Scenario: Create a blog post with markdown 89 | Given I am logged in as an author 90 | When I create a new blog post with content: 91 | """ 92 | # Introduction to Clojure 93 | 94 | Clojure is a dynamic, general-purpose programming language. 95 | 96 | ## Key Features 97 | - Functional programming 98 | - Immutable data structures 99 | - Runs on the JVM 100 | """ 101 | Then the post should be formatted as HTML 102 | And the title should be "Introduction to Clojure" 103 | ``` 104 | 105 | Doc strings are commonly used for: 106 | - JSON or XML payloads 107 | - Markdown or HTML content 108 | - Multi-line configuration 109 | - Email templates 110 | - Test data fixtures 111 | 112 | ## 2. Defining Feature References 113 | 114 | After writing the feature file, you need to reference it in your Clojure code using `deffeature`. 115 | 116 | ```clojure 117 | (ns my-project.shopping-cart-test 118 | (:require [clojure.test :refer :all] 119 | [scenari.v2.core :as scenari :refer [deffeature]])) 120 | 121 | ;; Reference to the feature file 122 | (deffeature shopping-cart "resources/features/shopping_cart.feature") 123 | ``` 124 | 125 | This creates a test that can be executed by Clojure's test runner. The `deffeature` macro: 126 | 127 | 1. Loads and parses the feature file 128 | 2. Creates a Clojure test that will execute all scenarios in the feature 129 | 3. Associates the feature with the current namespace for step discovery 130 | 131 | ### Configuration Options 132 | 133 | You can customize the feature execution with options: 134 | 135 | ```clojure 136 | (deffeature shopping-cart "resources/features/shopping_cart.feature" 137 | {:pre-run [#'setup-database] 138 | :post-run [#'teardown-database] 139 | :pre-scenario-run [#'setup-cart] 140 | :post-scenario-run [#'cleanup-cart] 141 | :default-scenario-state {:user-id "test-user"}}) 142 | ``` 143 | 144 | ## 3. Implementing Step Definitions (Glue Code) 145 | 146 | Step definitions (also called "glue code") connect the Gherkin steps with actual Clojure code. Scenari provides macros for defining these connections. 147 | 148 | ### Basic Step Definitions 149 | 150 | ```clojure 151 | (ns my-project.shopping-cart-test 152 | (:require [clojure.test :refer :all] 153 | [scenari.v2.core :as scenari :refer [deffeature defgiven defwhen defthen]])) 154 | 155 | (defgiven "I have an empty shopping cart" [state] 156 | (assoc state :cart [])) 157 | 158 | (defwhen "I add {string} book to the cart" [state book-title] 159 | (update state :cart conj {:title book-title :type :book})) 160 | 161 | (defthen "my cart should contain {number} item" [state item-count] 162 | (is (= item-count (count (:cart state)))) 163 | state) 164 | 165 | (defthen "the item should be {string} book" [state book-title] 166 | (is (= book-title (-> state :cart first :title))) 167 | state) 168 | ``` 169 | 170 | ### Parameter Handling 171 | 172 | Scenari supports various parameter types in step definitions: 173 | 174 | - `{string}`: Matches a quoted string and passes it as a String 175 | - `{number}`: Matches a number and passes it as a Number 176 | - Table data: Automatically passed as a vector of maps 177 | - Doc strings: Automatically passed as a multi-line string 178 | 179 | ### Working with Tables 180 | 181 | ```clojure 182 | (defgiven "the following products in catalog:" [state table-data] 183 | (assoc state :products 184 | (into {} (map (fn [row] [(:product row) row]) table-data)))) 185 | ``` 186 | 187 | ### Working with Doc Strings 188 | 189 | ```clojure 190 | (defwhen "I create a new blog post with content:" [state doc-string] 191 | ;; doc-string contains the multi-line text from the feature file 192 | (let [parsed-post (parse-markdown doc-string)] 193 | (assoc state :post {:content doc-string 194 | :title (extract-title parsed-post) 195 | :html (markdown->html doc-string)}))) 196 | ``` 197 | 198 | ### State Passing Between Steps 199 | 200 | Each step function receives the state from the previous step and must return the (possibly modified) state for the next step. This allows for data to flow through your scenario. 201 | 202 | ### Best Practices for Step Definitions 203 | 204 | - Keep step functions focused and small 205 | - Use descriptive step names 206 | - Include meaningful assertions 207 | - Don't couple steps too tightly to implementation details 208 | - Store state in a map for flexibility 209 | 210 | ## 4. Execution Flow 211 | 212 | When a feature is executed, Scenari performs the following steps: 213 | 214 | 1. **Feature Loading**: Parse the feature file into an AST 215 | 2. **Feature Transformation**: Convert the AST into an executable structure 216 | 3. **Scenario Execution**: For each scenario: 217 | - Initialize the scenario state (empty map or provided default) 218 | - Execute any pre-scenario hooks 219 | - For each step: 220 | - Find the matching step definition 221 | - Execute the step function with the current state and parameters 222 | - Capture the result and status 223 | - Pass the result state to the next step 224 | - Execute any post-scenario hooks 225 | 4. **Reporting**: Collect results and generate reports 226 | 227 | ### Step Matching Process 228 | 229 | The step matching process is a key part of Scenari: 230 | 231 | 1. Convert the step text from the feature file into a searchable format 232 | 2. Look for step definitions that match the pattern 233 | 3. If multiple matches are found, use namespace proximity to select the best match 234 | 4. Extract parameters from the step text 235 | 5. Execute the matching function with state and parameters 236 | 237 | ## 5. Execution and Validation 238 | 239 | ### Running Tests 240 | 241 | The simplest way to execute Scenari tests is through the standard Clojure test runner: 242 | 243 | ```bash 244 | clojure -M:test # Run all tests 245 | ``` 246 | 247 | Scenari integrates with Kaocha for more advanced test execution: 248 | 249 | ```bash 250 | clojure -M:test -m kaocha.runner # Run all tests 251 | clojure -M:test -m kaocha.runner --focus my-test # Run specific test 252 | ``` 253 | 254 | ### Test Output 255 | 256 | The test output will show each scenario and step execution: 257 | 258 | ``` 259 | --- my-project.shopping-cart-test --- 260 | Feature: Shopping Cart 261 | 262 | Testing scenario: Add item to empty cart 263 | Given I have an empty shopping cart 264 | When I add "Clojure Programming" book to the cart 265 | Then my cart should contain 1 item 266 | And the item should be "Clojure Programming" book 267 | 268 | PASS: my-project.shopping-cart-test/shopping-cart 269 | ``` 270 | 271 | ### Debugging Tests 272 | 273 | When a step fails, Scenari provides information about the failure: 274 | 275 | ``` 276 | Step failed: "my cart should contain 1 item" 277 | Expected: 1 278 | Actual: 0 279 | ``` 280 | 281 | The state passed between steps can be examined in the test output when there's a failure. 282 | 283 | ## 6. Advanced Features 284 | 285 | ### Namespace Resolution 286 | 287 | When multiple step definitions match a step, Scenari uses namespace proximity to choose: 288 | 289 | 1. Steps in the same namespace as the feature have highest priority 290 | 2. Steps in namespaces with more shared segments have higher priority 291 | 3. If equal priority, an error is raised to avoid ambiguity 292 | 293 | ### Custom Parameter Types 294 | 295 | You can extend Scenari with custom parameter types by creating specialized regex patterns in your step definitions. 296 | 297 | ### Hooks and Lifecycle Management 298 | 299 | Scenari supports several hook points for setup and teardown: 300 | 301 | - Pre-feature hooks: Run once before the entire feature 302 | - Post-feature hooks: Run once after the entire feature 303 | - Pre-scenario hooks: Run before each scenario 304 | - Post-scenario hooks: Run after each scenario 305 | 306 | ## Conclusion 307 | 308 | The Scenari development workflow provides a structured approach to Behavior-Driven Development in Clojure. By following the pattern of writing features, defining glue code, and executing tests, you can create living documentation that verifies your application's behavior. 309 | 310 | Remember that the true value of BDD comes from the collaborative process—use feature files as a communication tool between developers, testers, and domain experts to ensure a shared understanding of requirements and behaviors. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | # Scenari - Executable Specification / BDD in Clojure 7 | 8 | Scenari is an Executable Specification [Clojure](http://clojure.org/) library aimed at writing and executing usage scenarios following the [Behavior-Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) - BDD - style. It has an [external DSL](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html), following the [gherkin grammar](https://github.com/cucumber/cucumber/wiki/Gherkin) (in short: Given/When/Then), and execute each scenario's _steps_ with associated Clojure code. 9 | 10 | * [Installation](#installation) 11 | * [Basic Usage](#basic-usage) 12 | * [Write Scenarios in plain text]() 13 | * [Map steps to Clojure code]() 14 | * [Execute Specification and get a report]() 15 | * [Define "before" and "after" code]() 16 | * [Documentation](#documentation) 17 | * [Rationale](#rationale) 18 | * [ToDoS](#todos) 19 | 20 | ## Installation 21 | 22 | ```clojure 23 | ;;add this dependency to your project.clj file 24 | [io.defsquare/scenari "2.0.2"] 25 | ;;or deps.edn 26 | { 27 | io.defsquare/scenari {:mvn/version "2.0.2"} 28 | } 29 | ;;then in your ns statement 30 | (:require [scenari.v2.core :as scenari :refer [defgiven defwhen defthen deffeature]]) 31 | ``` 32 | 33 | [![Clojars Project](https://img.shields.io/clojars/v/io.defsquare/scenari.svg)](https://clojars.org/io.defsquare/scenari) 34 | 35 | ## Basic Usage 36 | 37 | 38 | ### Write Scenarios in plain text 39 | First, write your scenarios in plain text using the [Gherkin grammar]((https://github.com/cucumber/cucumber/wiki/Gherkin)) in a file or String : 40 | 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) 41 | ```Gherkin 42 | Scenario: create a new product 43 | # this is a comment 44 | When I create a new product with name "iphone 6" and description "awesome phone" 45 | Then I receive a response with an id 56422 and a location URL 46 | # this a second comment 47 | # on two lines 48 | When I invoke a GET request on location URL 49 | Then I receive a 200 response 50 | 51 | Scenario: get product info 52 | When I invoke a GET request on location URL 53 | Then I receive a 200 response 54 | ``` 55 | 56 | ### Declaring a specification 57 | 58 | ```clojure 59 | (require 'scenari.v2.core :refer [deffeature]) 60 | 61 | (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 62 | ;;=> 63 | ;;{:scenarios [{:id "0ef9b8a9-e035-4ae2-96c4-662c0b8988de", 64 | ;; :pre-run (), 65 | ;; :post-run (), 66 | ;; :scenario-name " create a new product", 67 | ;; :steps [{:sentence-keyword :when, 68 | ;; :sentence "I create a new product with name \"iphone 6\" and description \"awesome phone\"", 69 | ;; :raw "When I create a new product with name \"iphone 6\" and description \"awesome phone\"", 70 | ;; :params [{:type :value, :val "iphone 6"} {:type :value, :val "awesome phone"}], 71 | ;; :order 0, 72 | ;; :glue nil} 73 | ;; ...steps]} 74 | ;; ...scenarios ], 75 | ;; :pre-run ()} 76 | ``` 77 | ### Write glue-code 78 | 79 | Then write the code that will get executed for each scenario steps: 80 | 81 | ```clojure 82 | 83 | (require 'scenari.v2.core :refer [defwhen defthen]) 84 | 85 | (defwhen "I create a new product with name {string} and description {string}" 86 | [_ name desc] 87 | (println "executing my product creation function with params " name desc) 88 | (let [id (UUID/next.)] 89 | {:id (UUID/next. ) 90 | :name name 91 | :desc desc 92 | :qty (rand-int 50) 93 | :location-url (str "http://example.com/product/" id)})) 94 | 95 | 96 | (defthen "I receive a response with an id {string}" 97 | [_ id] 98 | (println (str "executing the assertion that the product has been created with the id " id)) 99 | id) 100 | ``` 101 | 102 | **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. 103 | Example: 104 | 105 | Executing the specification with the step sentence without any matching function: 106 | 107 | ```gherkin 108 | When I create a new product with name "iphone 6" and description "awesome phone" 109 | ``` 110 | 111 | will generate in the stdout the following step function skeleton: 112 | 113 | ``` 114 | Missing step for : When I create a new product with name "iphone 6" and description "awesome phone" 115 | (defwhen "I create a new product with name {string} and description {string}" [state arg0 arg1] (do "something")) 116 | ``` 117 | 118 | ### how to get data from the scenario into your step function 119 | 120 | 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](https://clojure.github.io/clojure/clojure.edn-api.html)) 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. 121 | 122 | **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. 123 | 124 | 125 | ### Execute scenario(s) 126 | There is three-way to execute scenarios, depending on your situation 127 | 128 | #### Tree execution 129 | 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 130 | 131 | ```Clojure 132 | (require 'scenari.v2.core :refer [run-feature run-features]) 133 | (run-feature #'my-specification) 134 | 135 | ;;OR 136 | (require 'scenari.v2.core :refer [run-scenarios]) 137 | (run-features #'my-specification) 138 | ``` 139 | 140 | The execution report will be returned, rely on same clojure data-structure returned by `scenari.v2.core/deffeature`. Will set : 141 | - final `:status` of scenario(s) execution 142 | - step `:status` as `pending` when not executed, `failed` when assertions fail or exception thrown, `success` at last 143 | - step `:input-state` as the value returned by the previous step executed (empty map for the first one) 144 | - step `:output-state` as the value returned by the current step 145 | 146 | This method is useful for debugging. 147 | 148 | #### Clojure-test execution 149 | Use clojure-test reporting system by printing execution. 150 | 151 | ```clojure 152 | (require 'scenari.v2.test :refer [run-feature]) 153 | (run-feature #'my-specification) 154 | ;; ________________________ 155 | ;; Feature : 156 | ;; 157 | ;; Testing scenario : 158 | ;; When I create a new product with name "iphone 6" and description "awesome phone" (from /") 159 | ;; Step failed 160 | ;; create a new product failed at step of 161 | ;; 162 | ;; Testing scenario : 163 | ;; When I invoke a GET request on location URL (from scenari.v2.glue/"I invoke a GET request on location URL") 164 | ;; =====> {:kix "lol"} 165 | ;; Then I receive a 200 response (from /"") 166 | ;; Step failed 167 | ;; get product info failed at step of 168 | ;; 169 | ;; ________________________ 170 | ;; 171 | ``` 172 | Useful to integrate a feature in a clojure test namespace 173 | 174 | 175 | #### Kaocha runner 176 | Kaocha is a test runner and handle test phase lifecycle. 177 | 178 | By defining a test type in your kaocha configuration file (`tests.edn` by default) like this 179 | ```clojure 180 | #kaocha/v1 181 | {:tests [{:id :scenario 182 | :type :kaocha.type/scenari 183 | :kaocha/source-paths ["src"] 184 | :kaocha/test-paths ["test/scenario"] 185 | :kaocha.type.scenari/glue-paths ["test/scenario/glue"]}]} 186 | ``` 187 | You are able to launch your scenario using kaocha repl utility function 188 | 189 | ```clojure 190 | (require 'kaocha.repl :as krepl) 191 | (krepl/run :scenario) 192 | 193 | ;; Testing scenario : create a new product 194 | ;; When I invoke a GET request on location URL (from scenari.v2.glue/"I invoke a GET request on location URL") 195 | ;; 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") 196 | ;; Then I receive a response with an id 56422 (from scenari.v2.glue/"I receive a response with an id 56422") 197 | ;; Then a location URL (from scenari.v2.glue/"a location URL") 198 | ;; 199 | ;; 200 | ;; 1 tests, 1 assertions, 0 failures. 201 | ;; => #:kaocha.result{:count 1, :pass 1, :error 0, :fail 0, :pending 0} 202 | ``` 203 | Suitable when using kaocha to manage test lifecycle. 204 | 205 | ### Using hooks 206 | By providing an options maps in `scenari.v2.core/deffeature`, you can specify function which execute : 207 | - `:pre-run` before feature execution 208 | - `:post-run` after feature executed 209 | - `:pre-scenario-run` before each scenario execution 210 | - `:post-scenaro-run` after each scenario executed 211 | Example: 212 | 213 | ```Clojure 214 | (require 'scenari.v2.core :refer [deffeature]) 215 | 216 | (defn before-all [] (prn "init feature components")) 217 | (defn before-each [] (prn "init scenario components")) 218 | (defn after-each [] (prn "clean scenario side effects")) 219 | (defn clean [] (prn "reset and shut down components")) 220 | 221 | (deffeature my-specification "./path/to/feature/file" 222 | {:pre-run [#'before-all] 223 | :pre-scenario-run [#'before-each] 224 | :post-scenario-run [#'after-each] 225 | :post-run [#'clean]}) 226 | ``` 227 | 228 | ### Provide an initial state 229 | For each scenario execution, an initial state can be provided within the options map of `deffeature`. 230 | 231 | Example: 232 | 233 | ```clojure 234 | (deffeature my-specification "./path/to/feature/file" 235 | {:default-scenario-state {:foo "bar"}}) 236 | ``` 237 | By default, the scenario state is an empty map `{}`. 238 | 239 | ## Documentation 240 | 241 | ### Development Workflow 242 | The [Development Workflow Guide](doc/development-workflow.md) 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. 243 | 244 | ### Internal Feature Structure 245 | The [Feature Structure Documentation](doc/feature-structure.md) provides a detailed explanation of the internal data structure used to represent features, scenarios, and steps in Scenari. This is particularly useful when extending or customizing Scenari. 246 | 247 | ### Declaring same step (glue-code) but different namespace 248 | Sometimes, you have to declare the same step (using the sentence matcher) but for different context (domain or component level for exemple). 249 | 250 | TODO Put an exemple about step proximity resolution 251 | 252 | ### Macros (deprecated) 253 | There are 3 macros available for given/when/then: 254 | 255 | ```clojure 256 | ;; a sentence for matching a step in the scenarios 257 | ;; a params vector: 258 | ;; the first param is the return of the previous step (nil if first step) 259 | ;; then one param for each sentence group (aka. something in parens (...)) you define. 260 | ;; a body function. 261 | 262 | (defgiven sentence-matcher params body) 263 | (defwhen sentence-matcher params body) 264 | (defthen sentence-matcher params body) 265 | ``` 266 | 267 | Macros are here for convenience, plain-old function are also available, you have to provide the step execution function as parameters with the function having groups count + 1 parameters (one for the previous step return and one params for each groups in the sentence matcher). 268 | 269 | ```clojure 270 | (ns mystuff 271 | (:require [spexec :as spec])) 272 | ... 273 | (Given sentence-matcher fn) 274 | (When sentence-matcher fn) 275 | (Then sentence-matcher fn) 276 | ``` 277 | 278 | ### Chaining steps 279 | Steps often produce side effect or retrieve some stuffs (fn, data) to be used in the next ones, you can store your state in your code or in the scenario itself, but I think an easier mechanism is to think of the steps like a chain and pass a data structure from the return of a step to the input of the next one (very similar to [ring handlers](https://github.com/ring-clojure/ring/wiki/Concepts) or chain of responsibility for instance). So, each step function's return is taken as the input for the next step as the first argument (you can name it `_` if you don't need it). A good practice would be to use a map or vector and then destructure it as the first param of the next step, like : 280 | 281 | ```clojure 282 | (defwhen "my sentence to be matched with {string} and {string}" 283 | [[key1-in-previous-result k2] param1 param2] 284 | (do-something param1 k2 param2) ...) 285 | ``` 286 | 287 | ### Advanced usage (deprecated) 288 | 289 | I use Spexec to test Spexec (yes it eats its own dog food, pretty amazing :-) only a dynamic language like Lisp can do that as easily), only the bootstrap step "Given the step function" is needed: 290 | 291 | ```gherkin 292 | Scenario: a scenario that test spexec using spexec 293 | Given the step function: (defgiven (re-pattern "^this scenario in a file named (.*)") [_ feature-file-name] [feature-file-name]) 294 | Given the step function: (defwhen (re-pattern "^I run the scenarios with '(.+)'") [prev-ret my-data] (conj prev-ret (str "processed" my-data))) 295 | Given the step function: (defthen (re-pattern "^I should get '(.+)' from scenario file '(.*)' returned from the previous when step") [prev-ret expected-data scenario-file] (clojure.test/is (= (last prev-ret) expected-data))(clojure.test/is (= (first prev-ret) scenario-file))) 296 | Given this scenario in a file named resources/spexec.feature 297 | When I run the scenarios with 'mydatavalues' 298 | Then I should get 'processedmydatavalues' from scenario file 'resources/spexec.feature' returned from the previous when step 299 | ``` 300 | 301 | ```clojure 302 | (defgiven "the step function: {string}" [_ step-fn] 303 | (eval (read-string step-fn))) 304 | ``` 305 | 306 | ### Run the scenarios with each steps 307 | 308 | ### Logging 309 | 310 | ## Rationale 311 | 312 | I'm used to [JBehave](http://jbehave.org/) and I wanted a BDD framework with an [external DSL](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html) following the [gherkin grammar](https://github.com/cucumber/cucumber/wiki/Gherkin) but also with an easy and fast setup and with steps written in [Clojure](http://clojure.org/). The previous BDD attempt I known in Clojure were all with an [internal DSL](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html). I prefer an external one because I think it's easier to share the scenarios with a domain expert. I you prefer an internal DSL BDD Framework, have a look at [Speclj](http://speclj.com/). 313 | 314 | Proper compatibility with traditional clojure.test interfaces is needed, for instance we could make the following relations between Scenari/BDD concepts and clojure.test: 315 | - A scenario execution is like a `deftest`. Particularly, we need to associate steps with Gherkin scenarios defined in one or more feature files. For that we would define this association between steps (`defgiven`, `defwhen` and `defthen`) and scenarios with `(defscenarios "my.feature")`. The execution would then be run with `(run-scenarios)` much like `(run-tests)`. In case of examples table used to feed the scenario with data, each data row would be associated with a new testing context for each steps (the `testing` context description would be the steps sentence with all data placeholder replaced with the actual ones). 316 | - A scenario's step is like a `testing` context inside a `deftest`. 317 | Also, steps and scenarios association must be isolated within namespace to avoid collisions when the same scenarios are used with different steps (like ones for domain test, others for integration testing, etc.). 318 | The compatibility with clojure.test would also be with its various reports available (`:pass`, `:fail`, etc.) with reports specific to narrative, scenarios and steps. 319 | Concerning assertion, steps could contains `clojure.test/is` assertions or throws exception that will be handled properly like clojure.test ones. 320 | 321 | The gherkin grammar parser is written with the amazing [Instaparse](https://github.com/Engelberg/instaparse) library (I thumbs up for the ClojureScript port by the way!). 322 | 323 | I did a presentation of the internals of the library at the Clojure Paris User Group and the slides are here: ["Anatomy of a BDD Execution Library in Clojure"](https://speakerdeck.com/jgrodziski/anatomy-of-a-bdd-execution-library-in-clojure). 324 | 325 | ## TODOS 326 | 327 | * stop-on-failure? as an option for execution 328 | * give another way to declare steps without macro (proper defn with `:scenari/regex` in meta) 329 | 330 | ## License 331 | 332 | Scenari is released under the terms of the [MIT License](http://opensource.org/licenses/MIT). 333 | 334 | Copyright © 2024 DefSquare defsquare.io 335 | 336 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 337 | 338 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 339 | 340 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 341 | --------------------------------------------------------------------------------