0Pricing
DevOps Bootcamp · Урок

Имитация команд и заглушки внешних инструментов

Переопределяйте 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.

Схема подмены:

  1. Создайте временный каталог (каталог с заглушками)
  2. Запишите исполняемый файл с тем же именем, что и у настоящей команды
  3. Добавьте этот каталог в начало PATH
  4. Запустите тестируемый скрипт — он вызовет Вашу заглушку, а не настоящий двоичный файл
  5. Удалите временный каталог после теста

Это работает без прав суперпользователя, без изменения системных файлов и без специальной платформы.

Создание каталога с заглушками

Стандартный подход использует 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 — локальная установка не требуется.

Все уроки этого курса

  1. Модульное тестирование функций с Bats-core
  2. Имитация команд и заглушки внешних инструментов
  3. Тестовые данные, временные окружения и покрытие
  4. Запуск тестов Shell в CI-конвейерах
← Назад к DevOps Bootcamp