Имитация команд и заглушки внешних инструментов
Переопределяйте PATH и создавайте поддельные исполняемые файлы, чтобы тестировать скрипты без обращения к реальным системам
«Имитация команд и заглушки внешних инструментов» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Зачем имитировать команды в тестах Bash
При тестировании скрипта Bash, который вызывает curl, aws, git или любой внешний инструмент, возникает проблема: настоящие вызовы обращаются к сети, изменяют состояние, требуют затрат или просто завершаются с ошибкой в окружении непрерывной интеграции, где эти инструменты не установлены.
Имитация означает замену настоящей команды фиктивной, которой Вы управляете. Ваша фиктивная команда (или заглушка) возвращает предсказуемые вывод и коды завершения, поэтому тест выполняется быстро, изолированно и воспроизводимо.
- Не требуется доступ к сети или облаку
- Тесты выполняются за миллисекунды, а не за секунды
- Можно имитировать ошибки, которые трудно вызвать в реальных системах
- Конвейеры непрерывной интеграции остаются чистыми и не зависят от внешних компонентов
Bash предоставляет для этого удивительно простой механизм: достаточно поместить фиктивный двоичный файл в каталог, расположенный в $PATH раньше настоящего.
Как работает поиск в PATH
Когда оболочка выполняет такую команду, как curl, она ищет совпадение в каждом каталоге из $PATH слева направо и запускает первое найденное.
Это означает, что если добавить в начало $PATH каталог с собственным скриптом curl, оболочка никогда не дойдёт до /usr/bin/curl.
Схема подмены:
- Создайте временный каталог (каталог с заглушками)
- Запишите исполняемый файл с тем же именем, что и у настоящей команды
- Добавьте этот каталог в начало
PATH - Запустите тестируемый скрипт — он вызовет Вашу заглушку, а не настоящий двоичный файл
- Удалите временный каталог после теста
Это работает без прав суперпользователя, без изменения системных файлов и без специальной платформы.
Создание каталога с заглушками
Стандартный подход использует mktemp -d для создания изолированного временного каталога заглушек. Каждый тест или набор тестов получает собственный каталог, что предотвращает загрязнение между тестами.
После завершения теста удалите каталог с помощью rm -rf. Команда trap гарантирует очистку даже при досрочном завершении теста из-за ошибки.
#!/usr/bin/env bash
# Setup a stub bin directory for testing
# Create the temp dir
STUB_BIN=$(mktemp -d)
# Always clean up on exit (success, error, or signal)
trap 'rm -rf "$STUB_BIN"' EXIT
# Prepend it to PATH so our stubs take priority
export PATH="$STUB_BIN:$PATH"
echo "Stub bin: $STUB_BIN"
echo "PATH starts with: ${PATH%%:*}"
# Your tests would go here...
echo "Tests complete."Написание первой заглушки
Заглушка — это всего лишь исполняемый файл с тем же именем, что и у заменяемой команды. Она выводит то, что ожидает тестируемый скрипт, и завершается с выбранным Вами кодом.
Основные правила для заглушек:
- Файл должен быть исполняемым (
chmod +x) - Строка shebang (
#!/usr/bin/env bash) обязательна - Выводите данные, которые настоящий скрипт обрабатывал бы
- Используйте
exit 0при успехе и ненулевой код для имитации ошибок
#!/usr/bin/env bash
# Create a stub for 'curl' that returns a fake HTTP response
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Write the stub
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
# Fake curl: always returns a 200 OK with a JSON body
echo '{"status": "ok", "version": "1.2.3"}'
exit 0
EOF
chmod +x "$STUB_BIN/curl"
# Verify the stub is found before the real curl
which curl
curl https://example.com/api/versionТестирование скрипта, вызывающего curl
Теперь объединим всё вместе. Предположим, у Вас есть скрипт развёртывания, который вызывает curl для проверки конечной точки состояния, а затем завершается с ошибкой, если служба работает неправильно. Вы хотите проверить и успешный сценарий, и сценарий сбоя без использования настоящего сервера.
#!/usr/bin/env bash
# Script under test: check_health.sh
# It calls curl and checks the returned JSON
check_health() {
local url="$1"
local response
response=$(curl -sf "$url")
if [[ "$response" == *'"healthy":true'* ]]; then
echo "Service is UP"
return 0
else
echo "Service is DOWN" >&2
return 1
fi
}
# ---- Test harness ----
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Happy path stub
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo '{"healthy":true}'
EOF
chmod +x "$STUB_BIN/curl"
check_health "http://fake-host/health" && echo "PASS: healthy response"Имитация сбоев команд
Одно из самых ценных применений заглушек — имитация сбоев, которые трудно воспроизвести с помощью настоящих инструментов: сетевых тайм-аутов, ошибок доступа, переполнения диска или ответа удалённого API с кодом 500.
Чтобы имитировать сбой, достаточно завершить работу заглушки с ненулевым кодом. Вы также можете записывать данные в stderr точно так же, как это делала бы настоящая команда, чтобы полностью проверить обработку ошибок в Вашем скрипте.
#!/usr/bin/env bash
# Test that check_health handles a curl failure gracefully
check_health() {
local url="$1"
local response
# -f makes curl exit non-zero on HTTP error; -s silences progress
if ! response=$(curl -sf "$url" 2>/dev/null); then
echo "ERROR: could not reach $url" >&2
return 1
fi
echo "OK: $response"
}
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Failure stub — simulates a network error (curl exit code 6 = could not resolve host)
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo 'curl: (6) Could not resolve host: fake-host' >&2
exit 6
EOF
chmod +x "$STUB_BIN/curl"
if ! check_health "http://fake-host/health"; then
echo "PASS: failure path handled correctly"
fiЗапись вызовов заглушки для проверки
Иногда нужно проверить не только что выводит Ваш скрипт, но и как он вызывает внешний инструмент: какие аргументы передаёт, сколько раз его вызывает и в каком порядке. Заглушка-шпион записывает свои вызовы в файл.
После выполнения теста тестовый каркас считывает файл с записью и проверяет его содержимое. Это позволяет проверять аргументы без специальной платформы.
#!/usr/bin/env bash
# Spy stub: record every invocation of 'aws' to a log file
STUB_BIN=$(mktemp -d)
CALL_LOG=$(mktemp)
trap 'rm -rf "$STUB_BIN" "$CALL_LOG"' EXIT
export PATH="$STUB_BIN:$PATH"
export CALL_LOG # make it available inside the stub
cat > "$STUB_BIN/aws" << 'EOF'
#!/usr/bin/env bash
# Append all arguments to the call log
echo "aws $*" >> "$CALL_LOG"
# Return fake S3 output
echo "upload: ./report.pdf to s3://my-bucket/report.pdf"
exit 0
EOF
chmod +x "$STUB_BIN/aws"
# Simulate the script under test calling aws s3 cp
aws s3 cp report.pdf s3://my-bucket/report.pdf
aws s3 cp logs.tar.gz s3://my-bucket/logs.tar.gz
# Verify calls were made with expected arguments
echo "--- Recorded calls ---"
cat "$CALL_LOG"
grep -q 's3://my-bucket/report.pdf' "$CALL_LOG" && echo "PASS: S3 upload verified"Имитация нескольких команд одновременно
Настоящий скрипт часто вызывает несколько внешних инструментов. Вы можете разместить заглушки для всех них в одном каталоге STUB_BIN. Каждый файл-заглушка независим и может возвращать разные результаты и коды завершения.
Делайте заглушки минимальными: возвращайте только то, что фактически разбирает проверяемый скрипт. Не пытайтесь имитировать каждый флаг — достаточно подмножества, которое использует Ваш скрипт.
#!/usr/bin/env bash
# Stub both 'git' and 'docker' for a release script test
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Stub git: pretend we are on tag v2.1.0
cat > "$STUB_BIN/git" << 'EOF'
#!/usr/bin/env bash
case "$*" in
*"describe --tags"*) echo "v2.1.0" ;;
*"rev-parse HEAD"*) echo "abc1234" ;;
*) echo "[git stub] unhandled: $*" >&2 ; exit 1 ;;
esac
EOF
chmod +x "$STUB_BIN/git"
# Stub docker: pretend build and push succeed
cat > "$STUB_BIN/docker" << 'EOF'
#!/usr/bin/env bash
echo "[docker stub] $*"
exit 0
EOF
chmod +x "$STUB_BIN/docker"
# Simulate the release logic
VERSION=$(git describe --tags)
SHA=$(git rev-parse HEAD)
echo "Building image for version=$VERSION sha=$SHA"
docker build -t "myapp:$VERSION" .
docker push "myapp:$VERSION"Использование функций в качестве заглушек (без файлов)
В простых случаях вообще не нужно создавать файлы. Вы можете определить функцию оболочки с тем же именем, что и команда. Поскольку функции разрешаются раньше внешнего поиска в PATH, они автоматически получают приоритет.
Это самый быстрый подход для модульного тестирования скриптов, загружаемых через source. Однако заглушки-функции работают только в том же процессе оболочки — они не будут видны дочерним оболочкам, запущенным с явным bash -c, или фоновым процессам. В таких случаях используйте файловый подход.
#!/usr/bin/env bash
# Source the script under test (a small helper library)
source_under_test() {
# Inline the logic we want to test
get_instance_id() {
# Would normally call: curl http://169.254.169.254/latest/meta-data/instance-id
curl -sf http://169.254.169.254/latest/meta-data/instance-id
}
}
source_under_test
# Override curl with a shell function stub
curl() {
echo "i-0abc123def456"
return 0
}
# Export is NOT needed — function is visible in same shell
# Run the function under test
result=$(get_instance_id)
[[ "$result" == "i-0abc123def456" ]] && echo "PASS: instance ID returned" || echo "FAIL"Экспорт функций в дочерние оболочки
Когда проверяемый скрипт запускает дочернюю оболочку (например, bash script.sh или конвейер), заглушки-функции оболочки, определённые в родительском процессе, по умолчанию не наследуются. У Вас есть два варианта:
- Используйте
export -f function_name, чтобы экспортировать функцию — она станет доступна дочерним процессамbash - Или используйте файловые заглушки в каталоге
STUB_BIN; они всегда работают между границами процессов
export -f — элегантное решение, но оно работает только с bash (не с sh и другими оболочками). В многоязычных средах непрерывной интеграции отдавайте предпочтение файловым заглушкам.
#!/usr/bin/env bash
# Demonstrate export -f for subshell-visible function stubs
# Define the stub in the current shell
curl() {
echo '{"status":"ok"}'
return 0
}
# Export the function so child bash processes inherit it
export -f curl
# Verify the stub works in a subshell
bash -c '
response=$(curl -sf http://api.example.com/status)
echo "Subshell got: $response"
'
# Without export -f, the subshell would call the real curl
# (or fail if curl is not installed)Интеграция заглушек с платформой тестирования (BATS)
При использовании BATS (системы автоматизированного тестирования Bash) настройка заглушек выполняется в обработчике setup(), а очистка — в teardown(). BATS сбрасывает окружение между тестами, поэтому каждый тест получает новый каталог заглушек.
Переменные BATS, например $BATS_TEST_TMPDIR, автоматически предоставляют временный каталог для каждого теста — используйте его вместо mktemp -d, чтобы код был чище.
#!/usr/bin/env bats
# File: test_deploy.bats
# Run with: bats test_deploy.bats
setup() {
# BATS provides a unique tmpdir per test
export STUB_BIN="$BATS_TEST_TMPDIR/stub_bin"
mkdir -p "$STUB_BIN"
export PATH="$STUB_BIN:$PATH"
# Default stub: healthy service
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo '{"healthy":true}'
EOF
chmod +x "$STUB_BIN/curl"
}
teardown() {
# BATS auto-removes BATS_TEST_TMPDIR, but explicit is safer
rm -rf "$STUB_BIN"
}
@test "deploy succeeds when service is healthy" {
run bash deploy.sh
[ "$status" -eq 0 ]
[[ "$output" == *"Deploy complete"* ]]
}
@test "deploy aborts when service is down" {
# Override the stub for this specific test
echo -e '#!/usr/bin/env bash\nexit 1' > "$STUB_BIN/curl"
chmod +x "$STUB_BIN/curl"
run bash deploy.sh
[ "$status" -ne 0 ]
}Проверка знаний: имитация команд в Bash
Проверьте, насколько хорошо Вы понимаете имитацию команд и шаблоны заглушек в Bash.
Разработчик пишет тест, в котором определяет функцию оболочки с именем aws, чтобы заменить настоящую команду AWS CLI. При непосредственном запуске в терминале тест работает, но когда конвейер непрерывной интеграции запускает проверяемый скрипт как bash deploy.sh, заглушка игнорируется и вызывается настоящая команда aws.
Как правильно исправить эту проблему?
Итоги: имитация команд и заглушки для внешних инструментов
Вы изучили полный набор приёмов для замены настоящих команд управляемыми имитациями при тестировании скриптов Bash.
Рассмотренные основные приёмы:
- Добавление в начало
PATH— создайте каталогSTUB_BINс помощьюmktemp -d, поместите туда исполняемые файлы-заглушки и добавьте этот каталог в началоPATH - Коды завершения заглушек — возвращайте
0при успехе и ненулевые значения для имитации конкретных сбоев (сетевых ошибок, отказа в доступе и т. д.) - Заглушки-шпионы — добавляйте аргументы в файл журнала внутри заглушки, чтобы проверять, как Ваш скрипт вызывал внешние инструменты
- Несколько заглушек — размещайте несколько файлов-заглушек в одном
STUB_BIN, чтобы одновременно имитировать всю экосистему зависимостей - Заглушки-функции — определяйте функцию оболочки с тем же именем, что и команда, для имитации в одном процессе; используйте
export -f, чтобы она была доступна дочерним процессам bash - Интеграция с BATS — используйте обработчики
setup()/teardown()и$BATS_TEST_TMPDIRдля чистой изоляции каждого теста
Всегда используйте trap '...' EXIT, чтобы гарантировать удаление заглушек независимо от результата теста. Делайте заглушки минимальными — возвращайте только то, что фактически разбирает Ваш скрипт. Файловые заглушки — самый переносимый вариант для конвейеров непрерывной интеграции.
Часто задаваемые вопросы
Урок «Имитация команд и заглушки внешних инструментов» бесплатный?
Да — полный текст урока «Имитация команд и заглушки внешних инструментов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Имитация команд и заглушки внешних инструментов»?
Переопределяйте PATH и создавайте поддельные исполняемые файлы, чтобы тестировать скрипты без обращения к реальным системам Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Имитация команд и заглушки внешних инструментов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Модульное тестирование функций с Bats-core
- Имитация команд и заглушки внешних инструментов
- Тестовые данные, временные окружения и покрытие
- Запуск тестов Shell в CI-конвейерах