Skip to content

Instantly share code, notes, and snippets.

@noel-yap
Created July 2, 2021 14:05
Show Gist options
  • Save noel-yap/5f985d89696788b3aca8a9c3c0b43c7e to your computer and use it in GitHub Desktop.
Save noel-yap/5f985d89696788b3aca8a9c3c0b43c7e to your computer and use it in GitHub Desktop.
bash mocking
setup() {
. "${BATS_TEST_DIRNAME}/bash-inject.shlib"
}
@test "should default to defining function" {
assert_equal "$(declare -F fn)" ''
@inject "${BATS_TEST_DIRNAME}/fn.sh"
run echo "$(declare -f fn)"
assert_output -p "${BATS_TEST_DIRNAME}/fn.sh"
}
@test "should not define recursive function" {
assert_equal "$(declare -F fn)" ''
@inject "fn"
run echo "$(declare -f fn)"
assert_output ''
}
@test "should use injected function" {
assert_equal "$(declare -F fn)" ''
function fn() {
echo mock fn
}
@inject "${BATS_TEST_DIRNAME}/fn.sh"
run echo "$(declare -f fn)"
assert_output -p 'echo mock fn'
}
# shellcheck disable=SC2148
# @inject allows an executable call to be mocked out by defining a function that calls the executable by default.
# A pre-defined function of the same name will take precedence thereby allowing mocking.
# Example:
# In sut-script.sh:
# @inject /path/to/dependency-executable.sh
#
# dependency-executable dependency-executable-args
#
# In sut-script.bats:
# function dependency-executable() {
# assert_equal "$1" 'arg1'
# }
#
# run sut-script sut-script-args
function @inject() {
executable="$1"
shift $#
fn="$(basename "${executable}" | sed -e 's|\..*||')"
quot='"'
declare -F "${fn}" >/dev/null ||
[ "${fn}" == "${executable}" ] ||
eval "function ${fn}() { ${executable} ${quot}\$@${quot}; }"
}
setup() {
. "${BATS_TEST_DIRNAME}/bash-mock.shlib"
}
@test "should use actual dependency" {
run "${BATS_TEST_DIRNAME}/testdata/sut-script.sh"
assert_success
assert_line 'first-call'
assert_line 'second-call'
}
@test "should use mock" {
@mock dependency-executable
function dependency-executable@0() {
assert_equal "$1" 'first-call'
echo 'mocked first-call'
}
function dependency-executable@1() {
assert_equal "$1" 'second-call'
echo 'mocked second-call'
}
run "${BATS_TEST_DIRNAME}/testdata/sut-script.sh"
assert_success
assert_line 'mocked first-call'
assert_line 'mocked second-call'
}
@test "should work when mock is called in subshells" {
@mock dependency-executable
function dependency-executable@0() {
assert_equal "$1" 'first-call'
echo 'mocked first-call'
}
function dependency-executable@1() {
assert_equal "$1" 'second-call'
echo 'mocked second-call'
}
run "${BATS_TEST_DIRNAME}/testdata/sut-script-with-subshell-calls.sh"
assert_success
assert_line 'mocked first-call'
assert_line 'mocked second-call'
}
# shellcheck disable=SC2148
# @mock creates a base mock function that delegates to other mock implementations.
# Example:
# In sut-script.sh:
# @inject dependency-executable
#
# dependency-executable dependency-executable-args
#
# In sut-script.bats:
# @mock dependency-executable
# # will be called on sut-script.sh's first call to dependency-executable
# function dependency-executable@0() {
# assert_equal "$1" 'first-call'
# }
# # will be called on sut-script.sh's second call to dependency-executable
# function dependency-executable@1() {
# assert_equal "$1" 'second-call'
# }
#
# run sut-script sut-script-args
# TODO(nyap): support each mock implementation being called a specified number of times; currently, each implementation is called only once
function @mock() {
tmpdir="$(mktemp -d --tmpdir="${BATS_TMPDIR}")"
name="$1"
counter_name="${name/-/_}_counter"
counter_filename="${tmpdir}/${counter_name}"
echo "${counter_name}=0" >"${counter_filename}"
quot='"'
# since the mock can be called from sub-shells, the counter state is stored in a file
eval "function ${name}() {
. ${counter_filename};
echo ${counter_name}=\$((${counter_name} + 1)) >${counter_filename};
${name}@\${${counter_name}} ${quot}\$@${quot};
}"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment