Skip to content

Instantly share code, notes, and snippets.

@noel-yap
Last active November 16, 2025 18:19
Show Gist options
  • Select an option

  • Save noel-yap/56402f161682cec0939f8d650ef7e0cf to your computer and use it in GitHub Desktop.

Select an option

Save noel-yap/56402f161682cec0939f8d650ef7e0cf to your computer and use it in GitHub Desktop.
Create a mock executable that returns a successive, caller‑provided behavior on each invocation
# shellcheck shell=bash
# mock_first_with_rest
# ---------------------
# Create a mock executable that returns a successive, caller‑provided behavior
# on each invocation, and persists an index so the next call advances to the
# next behavior. When all behaviors are exhausted, the mock prints an error to
# stderr and exits non‑zero. Re‑invoking this function for the same dependency
# resets the index back to 0.
#
# Why this exists
# - When testing shell scripts that call external executables ("dependencies"),
# it’s useful to simulate different outcomes across repeated calls: success,
# failure, specific outputs, etc. This helper generates a small mock program
# that plays back behaviors one by one.
#
# Runtime model
# - A hidden file named ".<dependency>.index" tracks which behavior to run.
# - Each time the generated mock runs, it:
# 1) Reads the current index
# 2) Executes the indexed behavior
# 3) Increments and persists the index regardless of success/failure
# 4) Exits with the behavior’s exit status
# - If the index is beyond the last behavior, it reports an error and exits 1.
#
# Usage
# mock_first_with_rest <dependency> <behavior1> [behavior2 ...]
#
# Parameters
# <dependency> The file name to create as the mock executable (e.g. "git").
# The function writes the script to "./<dependency>" and marks
# it executable. The index file used is ".<dependency>.index".
# <behaviorN> A shell snippet (string) or path to an executable to run.
# Behaviors are evaluated via `eval` inside a subshell
# so they can be simple one‑liners like 'echo ok; exit 0'.
#
# Notes
# - Re‑calling mock_first_with_rest with the same <dependency> overwrites the
# mock script and resets ".<dependency>.index" to 0.
# - Place the working directory containing the generated mock at the front of
# PATH when invoking the dependency under test.
#
# Examples
# # Example 1: inline behaviors
# mock_first_with_rest dependency \
# 'echo first; exit 0' \
# 'echo second; exit 7'
# PATH="${PWD}:${PATH}" dependency # prints "first", exits 0
# PATH="${PWD}:${PATH}" dependency # prints "second", exits 7
# PATH="${PWD}:${PATH}" dependency # prints error, exits 1 (exhausted)
#
# # Example 2: behaviors as helper scripts
# printf '%s\n' '#!/bin/sh' 'echo one' > dep-0 && chmod +x dep-0
# printf '%s\n' '#!/bin/sh' 'echo two' > dep-1 && chmod +x dep-1
# mock_first_with_rest dependency dep-0 dep-1
# PATH="${PWD}:${PATH}" dependency # "one"
# PATH="${PWD}:${PATH}" dependency # "two"
mock_first_with_rest() {
local -r _dependency="${1}"
shift
# Behaviors provided by caller (bash required)
local -a -r _behaviors=("$@")
local -r index_filename=".${_dependency}.index"
# Initialize index to 0 every time we (re)mock
echo '0' >"${index_filename}"
# Generate the mock script (/bin/sh)
# This script:
# - Reads the current index at runtime
# - Evaluates the selected behavior in a subshell to capture its exit code
# - Increments and persists the index regardless of behavior status
# - Exits with the behavior's exit code
# shellcheck disable=SC2016,SC2028
{
echo "#!/bin/bash"
echo "set -euo pipefail"
echo "readonly dependency='${_dependency}'"
echo "readonly index_filename='${index_filename}'"
echo 'readonly index="$(<"${index_filename}")"'
echo '_cleanup() {'
echo ' echo $((index + 1)) >"${index_filename}"'
echo '}'
# Ensure index increment on exit
echo 'trap _cleanup EXIT'
echo "readonly behaviors=( $(printf '"%s" ' "${_behaviors[@]}") )"
echo 'readonly behavior_count=${#behaviors[@]}'
echo 'if [ "${index}" -ge "${behavior_count}" ]; then'
echo ' printf "%s\n" "ERROR: ${dependency}: no more mocked behaviors (index ${index} >= ${behavior_count})" >&2'
echo ' exit 1'
echo 'else'
echo ' ( eval "${behaviors[${index}]}" )'
echo 'fi'
} | tee >"${_dependency}"
chmod +x "${_dependency}"
}
#!/bin/sh
# shellcheck shell=sh
eval "$(shellspec - -c) exit 1"
Describe 'mock_first_with_rest'
It 'initializes index file and creates a mock executable'
# Given
execute_sut_then_output_results() {
mock_first_with_rest dependency 'echo first; exit 0'
printf 'index=%s\n' "$(cat .dependency.index)"
ls dependency
}
When call in_tempdir execute_sut_then_output_results
The status should be success
The line 1 of stdout should equal 'index=0'
The line 2 of stdout should equal 'dependency'
The lines of stdout should equal 2
End
It 'returns successive behaviors on each invocation and increments index'
# Given
prepare_then_execute_sut() {
# shellcheck disable=SC2016
{
echo '#!/bin/sh'
echo 'echo INFO: "$(basename "$0")"'
} >dependency-0
chmod +x dependency-0
# shellcheck disable=SC2016
{
echo '#!/bin/sh'
echo 'echo INFO: "$(basename "$0")"'
} >dependency-1
chmod +x dependency-1
mock_first_with_rest dependency dependency-0 dependency-1
PATH="${PWD}:${PATH}" dependency
PATH="${PWD}:${PATH}" dependency
}
When call in_tempdir prepare_then_execute_sut
The status should be success
The line 1 of stdout should equal 'INFO: dependency-0'
The line 2 of stdout should equal 'INFO: dependency-1'
The lines of stdout should equal 2
End
It 'increments the index even on mock error'
run_calls() {
mock_first_with_rest dependency \
'echo first; exit 0' \
'echo second; exit 7'
chmod +x dependency
out1="$(bash ./dependency 2>&1)"; rc1=$?
out2="$(bash ./dependency 2>&1)"; rc2=$?
printf '%s\n' "${out1}"
printf '%s\n' "${rc1}"
printf '%s\n' "${out2}"
printf '%s\n' "${rc2}"
printf 'index=%s\n' "$(cat .dependency.index)"
}
When call in_tempdir run_calls
The status should be success
The line 1 of stdout should equal 'first'
The line 2 of stdout should equal '0'
The line 3 of stdout should equal 'second'
The line 4 of stdout should equal '7'
The line 5 of stdout should equal 'index=2'
The lines of stdout should equal 5
End
It 'fails with an error after behaviors are exhausted (non-zero exit status)'
run_exhausted() {
mock_first_with_rest dependency \
'echo only; exit 3'
chmod +x dependency
out1="$(bash ./dependency 2>&1)"; rc1=$?
out2="$(bash ./dependency 2>&1)"; rc2=$?
# Print placeholders for empty output to make assertions simple
[ -n "${out1}" ] && printf '%s\n' "${out1}" || printf '<empty>\n'
printf '%s\n' "${rc1}"
[ -n "${out2}" ] && printf '%s\n' "${out2}" || printf '<empty>\n'
printf '%s\n' "${rc2}"
printf 'index=%s\n' "$(cat .dependency.index)"
}
When call in_tempdir run_exhausted
The status should be success
The line 1 of stdout should equal 'only'
The line 2 of stdout should equal '3'
The line 3 of stdout should equal 'ERROR: dependency: no more mocked behaviors (index 1 >= 1)'
The line 4 of stdout should equal '1'
The line 5 of stdout should equal 'index=2'
The lines of stdout should equal 5
End
It 'resets the index when re-mocking the same dependency'
run_reset() {
mock_first_with_rest dependency \
'echo first-A; exit 0' \
'echo second-A; exit 0'
chmod +x dependency
_="$(bash ./dependency)"; _="$(bash ./dependency)"
# Re-run with different behaviors, which should reset index to 0
mock_first_with_rest dependency \
'echo first-B; exit 5' \
'echo second-B; exit 0'
out="$(bash ./dependency 2>&1)"; rc=$?
printf '%s\n' "${out}"
printf '%s\n' "${rc}"
printf 'index=%s\n' "$(cat .dependency.index)"
}
When call in_tempdir run_reset
The status should be success
The line 1 of stdout should equal 'first-B'
The line 2 of stdout should equal '5'
The line 3 of stdout should equal 'index=1'
The lines of stdout should equal 3
End
End
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment