0Pricing
DevOps Bootcamp · Урок

Модульное тестирование функций с Bats-core

Структурируйте файлы тестов, проверки и настройку с очисткой, чтобы проверять отдельные функции Bash

«Модульное тестирование функций с Bats-core» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.

Что такое Bats-core и зачем его использовать

Bats-core (Bash Automated Testing System) — фактический стандарт для модульного тестирования Bash. Он позволяет писать структурированные, воспроизводимые тесты для функций и скриптов оболочки — так же, как Вы использовали бы JUnit для Java или pytest для Python.

  • Каждый тест представляет собой блок @test с понятным для человека описанием.
  • Тест считается пройденным, если каждая команда внутри него возвращает код завершения 0.
  • Тест завершается с ошибкой при первом ненулевом коде завершения или при невыполнении проверки.
  • Вывод совместим с TAP, поэтому системы непрерывной интеграции (GitHub Actions, Jenkins, GitLab CI) понимают его без дополнительной настройки.

Установите Bats-core с помощью менеджера пакетов или клонируйте репозиторий:

# Install via git (recommended — always latest)
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local

# Or on macOS with Homebrew
brew install bats-core

# Verify installation
bats --version
# bats 1.x.y

Ваш первый файл тестов Bats

Файл тестов Bats имеет расширение .bats и начинается со специальной строки shebang. Основной строительный блок — директива @test, за которой следуют строка описания и блок команд.

  • Строка shebang #!/usr/bin/env bats сообщает оболочке, как выполнить файл.
  • Каждый блок @test является независимым тестовым случаем.
  • Вы можете запустить один файл с помощью bats my_tests.bats или целый каталог с помощью bats test/.

Ниже приведена минимальная структура файла тестов Bats:

#!/usr/bin/env bats
# File: test/hello.bats

@test "echo outputs the expected string" {
  result=$(echo "hello world")
  [ "$result" = "hello world" ]
}

@test "false command causes test to fail" {
  # Uncommenting the next line would make this test fail:
  # false
  true
}

Загрузка тестируемой функции с помощью «load»

В реальных проектах функции Bash находятся в файлах библиотек, а не непосредственно в файле тестов. Bats предоставляет вспомогательную команду load, которая подключает внешние файлы относительно каталога файла тестов.

  • load '../lib/math.sh' подключает файл перед запуском каждого теста.
  • После загрузки все функции, определённые в этом файле, становятся доступными в блоках тестов.
  • Храните функции библиотек в каталоге lib/, а тесты — в каталоге test/, чтобы чётко разделить их.

Пример структуры проекта и соответствующего теста:

# Project layout:
# lib/math.sh       <- functions to test
# test/math.bats    <- test file

# lib/math.sh
add() {
  echo $(( $1 + $2 ))
}

divide() {
  if [ "$2" -eq 0 ]; then
    echo "Error: division by zero" >&2
    return 1
  fi
  echo $(( $1 / $2 ))
}

# test/math.bats
#!/usr/bin/env bats

load '../lib/math.sh'

@test "add returns correct sum" {
  result=$(add 3 4)
  [ "$result" = "7" ]
}

Основные проверки: run, $status, $output

Команда run — основа тестирования в Bats. Вместо непосредственного выполнения команды заключите её в run: это сохранит её код завершения и вывод, не приводя к немедленному завершению теста с ошибкой.

  • $status — содержит код завершения последней команды run.
  • $output — содержит объединённый стандартный вывод последней команды run.
  • $lines — массив, каждый элемент которого содержит одну строку вывода (${lines[0]}, ${lines[1]} и т. д.).

Это позволяет проверять как успешные сценарии, так и сценарии с ошибками:

#!/usr/bin/env bats

load '../lib/math.sh'

@test "divide 10 by 2 returns 5" {
  run divide 10 2
  [ "$status" -eq 0 ]
  [ "$output" = "5" ]
}

@test "divide by zero returns exit code 1" {
  run divide 10 0
  [ "$status" -eq 1 ]
}

@test "divide by zero prints error message" {
  run divide 10 0
  # $output captures stderr too when redirected inside the function
  [[ "$output" == *"division by zero"* ]]
}

Использование bats-assert для выразительных проверок

Встроенные проверки с помощью [ ] работают, но выдают малоинформативные сообщения об ошибках. Вспомогательная библиотека bats-assert предоставляет выразительные функции проверок, которые точно указывают, что пошло не так.

  • assert_success — проверяет, что $status равен 0.
  • assert_failure — проверяет, что $status ненулевой.
  • assert_output — проверяет, что $output совпадает с заданной строкой.
  • assert_output --partial — проверяет, что вывод содержит подстроку.
  • refute_output --partial — проверяет, что вывод НЕ содержит подстроку.

Установите библиотеку, клонировав bats-core/bats-assert в каталог test/helpers/, а затем загрузите её:

#!/usr/bin/env bats

# Load bats-assert (cloned into test/helpers/bats-assert)
load 'helpers/bats-assert/load'
load '../lib/math.sh'

@test "add 5 and 3 gives 8" {
  run add 5 3
  assert_success
  assert_output "8"
}

@test "divide by zero fails with descriptive message" {
  run divide 9 0
  assert_failure
  assert_output --partial "division by zero"
}

@test "add does not output an error" {
  run add 1 1
  refute_output --partial "Error"
}

setup и teardown: обработчики жизненного цикла теста

Bats предоставляет две специальные функции — setup и teardown, — которые автоматически выполняются до и после каждого теста. Используйте их для подготовки и очистки общего состояния, чтобы каждый тест начинался в известном окружении.

  • setup() выполняется перед каждым отдельным блоком @test.
  • teardown() выполняется после каждого отдельного блока @test, даже если тест завершился с ошибкой.
  • Типичные задачи: создание временных каталогов, установка переменных окружения и удаление временных файлов после теста.
#!/usr/bin/env bats

load '../lib/fileutils.sh'

setup() {
  # Create a fresh temp directory before every test
  TEST_DIR=$(mktemp -d)
  export TEST_DIR
}

teardown() {
  # Always clean up, even on test failure
  rm -rf "$TEST_DIR"
}

@test "write_file creates a file with correct content" {
  run write_file "$TEST_DIR/hello.txt" "hello world"
  assert_success
  [ -f "$TEST_DIR/hello.txt" ]
  [ "$(cat "$TEST_DIR/hello.txt")" = "hello world" ]
}

@test "write_file fails when directory does not exist" {
  run write_file "/nonexistent/dir/file.txt" "data"
  assert_failure
}

setup_file и teardown_file: обработчики уровня набора тестов

Иногда ресурсы, требующие больших затрат, нужно настроить один раз для всего файла, а не перед каждым отдельным тестом. Для этого Bats предоставляет setup_file и teardown_file.

  • setup_file() выполняется один раз перед всеми тестами в файле.
  • teardown_file() выполняется один раз после всех тестов в файле.
  • Используйте BATS_FILE_TMPDIR (доступна автоматически), чтобы передавать данные между setup_file и тестами: обычные переменные не сохраняются в дочерних оболочках.

Типичный сценарий: один раз запустить имитацию сервера или собрать двоичный файл, а затем остановить её или удалить его в конце:

#!/usr/bin/env bats

setup_file() {
  # Build the project binary once for all tests in this file
  make build --silent
  export BINARY="$PWD/bin/myapp"
  echo "Binary built: $BINARY"
}

teardown_file() {
  # Remove the binary after all tests complete
  rm -f "$BINARY"
  echo "Cleaned up binary"
}

setup() {
  # Still runs before each individual test
  TEST_TMP=$(mktemp -d)
}

teardown() {
  rm -rf "$TEST_TMP"
}

@test "myapp --version outputs version string" {
  run "$BINARY" --version
  assert_output --partial "1.0"
}

Тестирование функций, изменяющих файлы

Очень распространённый сценарий — тестирование функций Bash, которые читают файловую систему или записывают в неё. Основной приём — использование временных каталогов (с помощью mktemp -d в setup), чтобы тесты никогда не затрагивали реальные файлы и не мешали друг другу.

  • Всегда работайте внутри $TEST_DIR (или $BATS_TEST_TMPDIR — эта переменная автоматически доступна в последних версиях Bats).
  • Используйте вспомогательную библиотеку bats-file для понятных проверок файлов, таких как assert_file_exists и assert_file_contains.
  • Никогда не задавайте пути вроде /tmp/myfile напрямую: параллельные запуски тестов будут конфликтовать.
#!/usr/bin/env bats

load 'helpers/bats-assert/load'
load 'helpers/bats-file/load'
load '../lib/fileutils.sh'

setup() {
  TEST_DIR="$BATS_TEST_TMPDIR"
}

# lib/fileutils.sh defines:
# append_line() { echo "$2" >> "$1"; }

@test "append_line adds a line to an existing file" {
  echo "first line" > "$TEST_DIR/log.txt"

  run append_line "$TEST_DIR/log.txt" "second line"
  assert_success

  assert_file_contains "$TEST_DIR/log.txt" "second line"
}

@test "append_line creates file if it does not exist" {
  run append_line "$TEST_DIR/new.txt" "hello"
  assert_success
  assert_file_exists "$TEST_DIR/new.txt"
}

Имитация внешних команд

Функции часто вызывают внешние программы, например curl, aws или git. В модульных тестах нужно проверять Вашу логику, а не настоящую внешнюю команду. Самый простой способ имитации в Bats — определить в setup функцию оболочки с тем же именем, что и у команды: она будет иметь приоритет над настоящим двоичным файлом.

  • Определите в setup функцию вроде curl() { echo 'mocked response'; return 0; } и экспортируйте её.
  • Используйте export -f curl, чтобы функция была видна в дочерних оболочках, создаваемых командой run.
  • Для более сложных сценариев можно также записать имитацию во временный файл в каталоге из PATH.
#!/usr/bin/env bats

load 'helpers/bats-assert/load'
load '../lib/network.sh'

# lib/network.sh defines:
# fetch_status() {
#   local url="$1"
#   local code
#   code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
#   echo "$code"
# }

setup() {
  # Override 'curl' with a mock function
  curl() {
    # Simulate a 200 OK response
    echo "200"
    return 0
  }
  export -f curl
}

@test "fetch_status returns 200 when curl reports 200" {
  run fetch_status "https://example.com"
  assert_success
  assert_output "200"
}

Пропуск тестов и добавление тегов

Не каждый тест можно запускать всегда: иногда требуется настоящее сетевое подключение, определённый инструмент или конкретная ОС. Bats предоставляет команду skip, позволяющую условно пропустить тест с информативным сообщением вместо того, чтобы закомментировать его или прервать набор тестов.

  • Вызовите skip "reason" в любом месте блока @test, чтобы пропустить этот тест.
  • Пропущенные тесты отображаются в выводе как S и не считаются ошибками.
  • Bats 1.5+ поддерживает теги: добавляйте к тестам аннотацию # bats test_tags=slow,network, а затем фильтруйте их с помощью bats --filter-tags network test/.
#!/usr/bin/env bats

# bats test_tags=network
@test "API returns valid JSON" {
  # Skip if no internet connectivity
  if ! ping -c1 -W1 8.8.8.8 &>/dev/null; then
    skip "No network connection available"
  fi

  run curl -s "https://api.example.com/health"
  assert_success
  assert_output --partial '"status"'
}

# bats test_tags=unit
@test "slug function lowercases and replaces spaces" {
  # Always runs — pure function, no external deps
  slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-'; }
  run slug "Hello World"
  assert_output "hello-world"
}

# Run only unit tests:
# bats --filter-tags unit test/

Структура полного набора тестов

Хорошо организованный проект Bats имеет предсказуемую структуру каталогов, благодаря чему новых участников легко подключать к работе, а проект — интегрировать с конвейерами непрерывной интеграции.

Рекомендуемая структура:

  • lib/ — рабочие функции Bash (по одному файлу на каждую область ответственности: math.sh, fileutils.sh).
  • test/ — отдельный файл .bats для каждого файла библиотеки (math.bats, fileutils.bats).
  • test/helpers/ — bats-assert, bats-file и bats-support в качестве подмодулей git.
  • Makefile — цель test, чтобы участникам было достаточно выполнить make test.

Запустите весь набор одной командой:

# Makefile
.PHONY: test
test:
	bats test/

# Run all tests recursively (Bats 1.5+)
# bats --recursive test/

# Run a specific file
# bats test/math.bats

# Run with verbose (TAP) output for CI
# bats --tap test/

# Example directory tree:
# .
# |-- lib/
# |   |-- math.sh
# |   `-- fileutils.sh
# |-- test/
# |   |-- helpers/
# |   |   |-- bats-assert/
# |   |   `-- bats-file/
# |   |-- math.bats
# |   `-- fileutils.bats
# `-- Makefile

Проверка знаний: проверки Bats-core

Проверьте, насколько Вы поняли основной механизм тестирования в Bats-core.

Итоги: модульное тестирование Bash с Bats-core

В этом уроке Вы научились структурировать и писать модульные тесты для отдельных функций Bash с помощью Bats-core. Ниже перечислены основные выводы:

  • Структура файла тестов — используйте строку shebang #!/usr/bin/env bats и блоки @test с описательными именами.
  • load — подключайте файлы библиотек, чтобы функции были доступны в тестах без копирования кода.
  • run + $status + $output — основная тройка; всегда используйте run, чтобы сохранять результаты, не вызывая немедленного завершения теста с ошибкой.
  • bats-assert — отдавайте предпочтение assert_success, assert_failure и assert_output, а не обычным проверкам [ ], чтобы получать понятные сообщения об ошибках.
  • setup / teardown — выполняются до и после каждого теста; setup_file / teardown_file выполняются один раз для каждого файла.
  • Имитация — заменяйте внешние команды одноимёнными функциями оболочки, экспортированными с помощью export -f.
  • skip — условно пропускайте тесты, зависящие от недоступных ресурсов.
  • Структура проекта — разделяйте lib/, test/ и test/helpers/, чтобы упростить сопровождение и интеграцию с непрерывной интеграцией.

Эти приёмы обеспечивают в проектах Bash такую же дисциплину тестирования, какую Вы применяли бы в любом современном программном проекте.

Часто задаваемые вопросы

Урок «Модульное тестирование функций с Bats-core» бесплатный?

Да — полный текст урока «Модульное тестирование функций с Bats-core» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.

Чему я научусь в уроке «Модульное тестирование функций с Bats-core»?

Структурируйте файлы тестов, проверки и настройку с очисткой, чтобы проверять отдельные функции Bash Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DevOps Bootcamp?

Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Модульное тестирование функций с Bats-core»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DevOps Bootcamp?

Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

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

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