diff --git a/README.adoc b/README.adoc index bbc2aab..a723311 100644 --- a/README.adoc +++ b/README.adoc @@ -12,7 +12,7 @@ bash_unit - bash unit testing enterprise edition framework for professionals! == Synopsis -*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] +*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] [-q] [-g] == Description @@ -76,6 +76,10 @@ _(by the way, the documentation you are reading is itself tested with bash-unit) Will only output the status of each test with no further information even in case of failure. +*-g*:: + gherkin style. + Accept tests written in a Gherkin like style (see "Gherkin" section below). + ifndef::backend-manpage[] == How to install *bash_unit* @@ -994,3 +998,122 @@ test_get_data_from_fake() { expected [ax] but was [a] doc:13:test_get_data_from_fake() ``` + +=== Write tests in a Gherkin like style +*bash_unit* supports to tests written in a +[Gherkin](https://cucumber.io/docs/gherkin/reference) like style. You need +to run *bash_unit* with the option _-g_ to enable the Gherkin like test style. + +```test-g +@FEATURE wc can count the number of lines + +@SCENARIO calling wc without any arguments should also count the # of lines +{ + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc + output=$((echo BDD; echo is; echo so; echo cool) | wc) + @THEN it should print three results + assert_equals 3 "$(echo $output | wc -w)" @MSG + @AND the first should match the number of lines (4) + assert_equals 4 "$(echo $output | cut -d " " -f 1)" @MSG +} +``` + +```output-g + Running test_calling_wc_without_any_arguments_should_also_count_the___of_lines ... SUCCESS +``` +In case of a failure (`wc -c` counts the number of characters instead of lines), +the resulting message is a merge of the individual `@XXX` decorators: + +```test-g +@SCENARIO calling wc with the argument -c it should count the # of lines +{ + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc -c + output=$((echo BDD; echo is; echo so; echo cool) | wc -c) + @THEN the counted number of lines should be 4 + assert_equals 4 "$(echo $output)" @MSG +} +``` + +```output-g + Running test_calling_wc_with_the_argument__c_it_should_count_the___of_lines ... FAILURE +SCENARIO calling wc with the argument -c it should count the # of lines + GIVEN wc is installed + WHEN running (echo BDD; echo is; echo so; echo cool) | wc -c + THEN the counted number of lines should be 4 + expected [4] but was [15] +doc:4:test_calling_wc_with_the_argument__c_it_should_count_the___of_lines() +``` + +The Gherkin like test style is implemented by a simple DSL. The test script +is preprocessed and lines with the `@XXX` decorators are getting replaced. +The resulting script is then tested by *bash_unit* as usual. + +Key concepts you need to keep in mind when using the Gherin stype DSL: + +* The preprocessor parses the test script line by line and substitues the +lines starting with a `@XXX` decorator. Leading whitespaces are ignorred. +* The `@MSG` decorator is handled in-line +* The text following an `@XXX` decorator (with the exception of the `@MSG` decorator) +is used as an argument to the decorator. +* A decorator entry ends at the lineend. There is no line continuation. +* Gherkin like tests can be mixed with the standard *bash_uni* style. +* Only upper case decorators are handled. +* Lines with an unknown `@XXX`decorator are filtered out. + +==== Recommended structure +The decorators helps to structure your test suites. Using the decorators +give ou some guidelines to your hand how to document your tests. The +structured approach is much more readable then looking into some comments. +When following a behavior driven develoment approach, the decorators can also +be used to derive the documentation of the behavior. + +```text +$ grep '^[[:blank:]]*@[[:upper:]]*' ../README.adoc +@FEATURE wc can count the number of lines +@SCENARIO calling wc without any arguments should also count the # of lines + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc + @THEN it should print three results + @AND the first should match the number of lines (4) +... +``` + +Begin your tests with a `@FEATURE` decorator to express that the following +set of test functions are used to verify the implemntation of a given feature. +The argument is stored in an internal variable but not used anymore. + +Beginning groups of tests with a `@FEATURE` helps to structure and to understand +the test by others or after some time. + +The `@SCENARIO` decorator is mainly the wrapper to define a test function. The +argument is any aspect to test given as clear text. It is used to generate name +of the test function. Each character other than a letter or digit is replayed by +an `_`. The resulting string is prefixed by `test_`. + +A line like +```text +@SCENARIO calling wc without any arguments should also count the # of lines +``` + +will be raplaced by +```text +test_calling_wc_without_any_arguments_should_also_count_the___of_lines () +``` + +Use the `@GIVEN` decorator to describe a precondition to be met. The line +will be filtered out and the argument will be used by the `@MSG` decorator. + +The `@WHEN` decorator should be used before running the code to be tested. +It should describe what is getting tested. The ine is filtered out and +used by the `@MSG` decorator. + +Usually, after calling the code to test the test expression follows. You +should add a `@THEN` decorator to express the expected behaviour of the +code to test. Again, the line is getting filtered out and the argument is +used by the `@MSG` decorator. + +If you have multiple test expression for a given call of the code to test +then you should use the `@AND` decorator. It is mainly the same as the `@THEN` +decorator but extends the readability of the test. diff --git a/bash_unit b/bash_unit index adc949d..aa6d39d 100755 --- a/bash_unit +++ b/bash_unit @@ -208,7 +208,12 @@ stacktrace() { local i=1 while [ -n "${BASH_SOURCE[$i]:-}" ] do - echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + if [ "$gherkin" = 1 ] && [[ ${BASH_SOURCE[$i]} =~ ^/dev/fd ]] + then + echo "$test_file:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + else + echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + fi i=$((i + 1)) done | "$GREP" -v "^$BASH_SOURCE" } @@ -504,6 +509,67 @@ count() { fi } +#--------------------- +gherkin_strip_arg() +{ + local s + s=${1## } + s=${s#\"} + s=${s%\"} + echo "$s" +} + +gherkin_FEATURE() { + gherkin_last_feature=$(gherkin_strip_arg "$*") + echo "" +} + +gherkin_SCENARIO() { + local func_name + gherkin_last_scenario=$(gherkin_strip_arg "$*") + func_name="${gherkin_last_scenario//[^a-zA-Z0-9_]/_}" + echo "test_$func_name ()" +} + +gherkin_GIVEN() { + gherkin_last_given=$(gherkin_strip_arg "$*") + echo "" +} + +gherkin_WHEN() { + gherkin_last_when=$(gherkin_strip_arg "$*") + echo "" +} + +gherkin_THEN() { + local COL="" + local NCOL="" + if is_terminal; then + COL="${YELLOW}" + NCOL="${NOCOLOR}" + fi + + gherkin_last_then=$(gherkin_strip_arg "$*") + gherkin_last_msg="" + gherkin_last_msg="${gherkin_last_msg}${COL}SCENARIO${NCOL} $gherkin_last_scenario\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}GIVEN${NCOL} $gherkin_last_given\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}WHEN${NCOL} $gherkin_last_when\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" + echo "" +} + +gherkin_AND() { gherkin_THEN "$@"; } + +gherkin_parse() +{ + while IFS= read -r line; do + # echo "$line" + [[ $line =~ [:blank:]*@([A-Z]*)\ (.*) ]] && { "gherkin_${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; continue; } + line="${line/@MSG/\"$gherkin_last_msg\"}" + echo "$line" + done < <(cat "$1") +} +#---------------------- output_format=text verbosity=normal test_pattern="" @@ -511,7 +577,8 @@ test_pattern_separator="" skip_pattern="" skip_pattern_separator="" randomize=0 -while getopts "vp:s:f:rq" option +gherkin=0 +while getopts "vp:s:f:rqg" option do case "$option" in p) @@ -535,6 +602,9 @@ do q) verbosity=quiet ;; + g) + gherkin=1 + ;; ?) usage ;; @@ -579,11 +649,23 @@ do if [[ "${STICK_TO_CWD:-}" != true ]] then cd "$(dirname "$test_file")" - # shellcheck disable=1090 - source "$(basename "$test_file")" + if [ "$gherkin" = 1 ] + then + # shellcheck disable=1090 + source <(gherkin_parse "$(basename "$test_file")") + else + # shellcheck disable=1090 + source "$(basename "$test_file")" + fi else - # shellcheck disable=1090 - source "$test_file" + if [ "$gherkin" = 1 ] + then + # shellcheck disable=1090 + source <(gherkin_parse "$test_file") + else + # shellcheck disable=1090 + source "$test_file" + fi fi set +e run_test_suite diff --git a/tests/test_doc.sh b/tests/test_doc.sh index 47d086b..00aea21 100644 --- a/tests/test_doc.sh +++ b/tests/test_doc.sh @@ -8,9 +8,13 @@ unset LC_ALL LANGUAGE export STICK_TO_CWD=true BASH_UNIT="eval FORCE_COLOR=false ./bash_unit" +TEST_PATTERN_GHERKIN='```test-g' +OUTPUT_PATTERN_GHERKIN='```output-g' +BASH_UNIT_GHERKIN="eval FORCE_COLOR=false ./bash_unit -g" +block=0 + prepare_tests() { - mkdir /tmp/$$ - local block=0 + [ -d /tmp/$$ ] || mkdir /tmp/$$ local remaining=/tmp/$$/remaining local swap=/tmp/$$/swap local test_output=/tmp/$$/test_output @@ -28,6 +32,14 @@ prepare_tests() { done } +prepare_gherkin_tests() +{ + BASH_UNIT="$BASH_UNIT_GHERKIN" + TEST_PATTERN="$TEST_PATTERN_GHERKIN" + OUTPUT_PATTERN="$OUTPUT_PATTERN_GHERKIN" + prepare_tests +} + function run_doc_test() { local remaining="$1" local swap="$2" @@ -81,3 +93,4 @@ function _next_quote_section() { # test subdirectory cd .. prepare_tests +prepare_gherkin_tests