├── .envrc ├── README.md ├── lib └── fizzbuzz.bash ├── script └── tdd └── test ├── fizzbuzz_test.bash └── test_helpers /.envrc: -------------------------------------------------------------------------------- 1 | PATH_add ./script 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDD Bash 2 | 3 | Even though shell scripting was present in every development team that I have worked in, it was almost never tested. Over time, everyone becomes afraid to change code without tests because it might break. This fear grows proportionally with the importance of the code. 4 | 5 | This repository is meant to capture the mechanics and reasoning behind test-driven bash code. It also conveys the process behind evolving a good bash codebase. 6 | 7 | ## How to write good bash code? 8 | 9 | 1. Understand the [bash pitfalls](http://mywiki.wooledge.org/BashPitfalls) 10 | 1. Use [shellcheck](http://www.shellcheck.net/) 11 | 1. Emphasise on functional code 12 | 1. Discover the simplest concepts 13 | 1. Explore their optimal composition 14 | 1. Use containers for integration testing 15 | 1. Stop when the fun stops 16 | -------------------------------------------------------------------------------- /lib/fizzbuzz.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | fizzbuzz() { 4 | local number numbers result 5 | numbers=($@) 6 | 7 | for number in "${numbers[@]}" 8 | do 9 | result="$(fizz "$number")$(buzz "$number")" 10 | echo "${result:-$number}" 11 | done 12 | } 13 | 14 | fizz() { 15 | is_factor_of "$1" 3 && echo "Fizz" 16 | } 17 | 18 | buzz() { 19 | is_factor_of "$1" 5 && echo "Buzz" 20 | } 21 | 22 | is_factor_of() { 23 | [ $(($1 % $2)) = 0 ] 24 | } 25 | -------------------------------------------------------------------------------- /script/tdd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -z "$DEBUG" ] || set -x 4 | 5 | main() { 6 | resolve_dependencies 7 | 8 | if [[ "$@" =~ ^w ]] 9 | then 10 | autotest 11 | else 12 | test_all 13 | fi 14 | } 15 | 16 | resolve_dependencies() { 17 | [[ "$(brew list)" =~ fsw ]] || brew install fsw 18 | 19 | go get github.com/progrium/basht 20 | } 21 | 22 | autotest() { 23 | local file 24 | 25 | fsw --latency 0.2 -i '.bash' -e '.' lib test | while read -r file 26 | do 27 | is_test_file? "$file" || file="test/$(basename -s '.bash' "$file")_test.bash" 28 | test_one "$file" && date 29 | done 30 | } 31 | 32 | is_test_file?() { 33 | local file 34 | file="$1" 35 | 36 | [[ "$file" =~ _test.bash$ ]] 37 | } 38 | 39 | test_all() { 40 | basht test/*_test.bash 41 | } 42 | 43 | test_one() { 44 | local file 45 | file="$1" 46 | 47 | [ -f "$file" ] && basht "$file" 48 | } 49 | 50 | main "$@" 51 | -------------------------------------------------------------------------------- /test/fizzbuzz_test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # basht macro, shellcheck fix 4 | export T_fail 5 | 6 | # shellcheck disable=SC1091 7 | . test/test_helpers 8 | 9 | # shellcheck disable=SC1091 10 | . lib/fizzbuzz.bash 11 | 12 | T_fizzbuzz_GivenASeriesOfNumbersAsParameters() { 13 | local actual expected 14 | actual="$(fizzbuzz {3..15})" 15 | expected="Fizz 16 | 4 17 | Buzz 18 | Fizz 19 | 7 20 | 8 21 | Fizz 22 | Buzz 23 | 11 24 | Fizz 25 | 13 26 | 14 27 | FizzBuzz" 28 | 29 | expect_to_equal "$actual" "$expected" || 30 | $T_fail 31 | } 32 | 33 | T_fizz_ReturnsFizzWhenDivisibleBy3() { 34 | expect_to_equal "$(fizz 15)" "Fizz" || 35 | $T_fail 36 | } 37 | 38 | T_buzz_ReturnsBuzzWhenDivisibleBy5() { 39 | expect_to_equal "$(buzz 15)" "Buzz" || 40 | $T_fail 41 | } 42 | 43 | T_is_factor_of_TrueWhenDividesEqually() { 44 | is_factor_of 3 3 || 45 | $T_fail "Expected to be true when it divides equally" 46 | } 47 | 48 | T_is_factor_of_FalseWhenDoesNotDivideEqually() { 49 | ! is_factor_of 3 5 || 50 | $T_fail "Expected to be false when it does not divide equally" 51 | } 52 | -------------------------------------------------------------------------------- /test/test_helpers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | expected() { 4 | echo -e "EXPECTED: \033[93m$*\033[0m" 5 | } 6 | 7 | not_expected() { 8 | echo -e "NOT EXPECTED: \033[91m$*\033[0m" 9 | } 10 | 11 | actual() { 12 | echo -e "ACTUAL: \033[93m$*\033[0m" 13 | } 14 | 15 | expect_to_equal() { 16 | local actual expected diff_output diff_exit 17 | actual="$1" 18 | expected="$2" 19 | 20 | diff_output="$(diff <(echo "$actual") <(echo "$expected"))" 21 | diff_exit=$? 22 | 23 | if [[ $diff_exit != 0 ]] 24 | then 25 | echo -e "$diff_output" 26 | return $diff_exit 27 | fi 28 | } 29 | 30 | expect_to_contain() { 31 | local haystack="$1" 32 | local needle="$2" 33 | 34 | # shellcheck disable=SC2076 35 | if ! [[ "$haystack" =~ "$needle" ]] 36 | then 37 | actual "$haystack" 38 | expected "$needle" 39 | return 1 40 | fi 41 | } 42 | 43 | expect_to_not_contain() { 44 | local haystack="$1" 45 | local needle="$2" 46 | 47 | # shellcheck disable=SC2076 48 | if [[ "$haystack" =~ "$needle" ]] 49 | then 50 | actual "$haystack" 51 | not_expected "$needle" 52 | return 1 53 | fi 54 | } 55 | 56 | enable_fakes() { 57 | export PATH="test/bin:$PATH" 58 | } 59 | 60 | pending() { 61 | echo -e "\033[33m~~~ PENDING\033[0m" 62 | } 63 | 64 | run_after_all_tests() { 65 | AFTER_ALL_TESTS+=("$1") 66 | } 67 | 68 | _after_all_tests() { 69 | local command 70 | for command in "${AFTER_ALL_TESTS[@]}" 71 | do 72 | "$command" 73 | done 74 | } 75 | 76 | trap _after_all_tests EXIT 77 | --------------------------------------------------------------------------------