Модульное тестирование функций с 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 — локальная установка не требуется.
Все уроки этого курса
- Модульное тестирование функций с Bats-core
- Имитация команд и заглушки внешних инструментов
- Тестовые данные, временные окружения и покрытие
- Запуск тестов Shell в CI-конвейерах