0Pricing
DevOps Bootcamp · Lekcja

Testowanie jednostkowe funkcji za pomocą Bats-core

Organizuj pliki testów, asercje oraz konfigurację i sprzątanie, aby weryfikować pojedyncze funkcje Bash.

Testowanie jednostkowe funkcji za pomocą Bats-core to bezpłatna lekcja DevOps Bootcamp na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DevOps Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.

Czym jest Bats-core i dlaczego warto go używać

Bats-core (Bash Automated Testing System) to de facto framework do testów jednostkowych dla Bash. Umożliwia pisanie uporządkowanych, powtarzalnych testów funkcji i skryptów powłoki — tak samo jak używa się JUnit w Javie lub pytest w Pythonie.

  • Każdy test jest blokiem @test z opisem zrozumiałym dla człowieka.
  • Test przechodzi, gdy każde polecenie w jego obrębie zwróci kod wyjścia 0.
  • Test kończy się niepowodzeniem przy pierwszym niezerowym kodzie wyjścia lub nieudanej asercji.
  • Dane wyjściowe są zgodne z TAP, dlatego systemy CI (GitHub Actions, Jenkins, GitLab CI) natywnie je rozumieją.

Zainstaluj Bats za pomocą menedżera pakietów lub sklonuj repozytorium:

# 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

Twój pierwszy plik testów Bats

Plik testów Bats ma rozszerzenie .bats i zaczyna się od specjalnego shebangu. Podstawowym elementem jest dyrektywa @test, po której występuje ciąg znaków z opisem oraz blok poleceń.

  • Shebang #!/usr/bin/env bats informuje powłokę, jak wykonać plik.
  • Każdy blok @test jest niezależnym przypadkiem testowym.
  • Pojedynczy plik można uruchomić za pomocą bats my_tests.bats, a cały katalog za pomocą bats test/.

Poniżej przedstawiono minimalną strukturę pliku testów 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
}

Wczytywanie testowanej funkcji za pomocą „load”

W rzeczywistych projektach funkcje Bash znajdują się w plikach bibliotek, a nie bezpośrednio w pliku testów. Bats udostępnia pomocnicze polecenie load, które wczytuje zewnętrzne pliki względem katalogu pliku testów.

  • load '../lib/math.sh' wczytuje plik przed uruchomieniem każdego testu.
  • Po wczytaniu wszystkie funkcje zdefiniowane w tym pliku są dostępne w blokach testów.
  • Dla zachowania przejrzystego podziału należy przechowywać funkcje biblioteczne w katalogu lib/, a testy w katalogu test/.

Przykładowy układ projektu i odpowiadający mu 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" ]
}

Podstawowe asercje: run, $status, $output

Polecenie run jest najważniejszym elementem testów Bats. Zamiast wykonywać polecenie bezpośrednio, należy opakować je w run. Wtedy jego kod wyjścia i dane wyjściowe zostaną przechwycone, a test nie zakończy się natychmiast niepowodzeniem.

  • $status — zawiera kod wyjścia ostatniego polecenia run.
  • $output — zawiera połączony standardowy strumień wyjścia ostatniego polecenia run.
  • $lines — tablica, której każdy element zawiera jeden wiersz danych wyjściowych (${lines[0]}, ${lines[1]} itd.).

Dzięki temu można sprawdzać zarówno przypadki powodzenia, jak i niepowodzenia:

#!/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"* ]]
}

Używanie bats-assert do wyrażistych asercji

Wbudowane asercje [ ] działają, ale generują mało pomocne komunikaty o niepowodzeniu. Biblioteka pomocnicza bats-assert udostępnia wyrażiste funkcje asercji, które dokładnie informują, co poszło nie tak.

  • assert_success — sprawdza, czy $status ma wartość 0.
  • assert_failure — sprawdza, czy $status ma wartość niezerową.
  • assert_output — sprawdza, czy $output jest równy podanemu ciągowi znaków.
  • assert_output --partial — sprawdza, czy dane wyjściowe zawierają podciąg.
  • refute_output --partial — sprawdza, czy dane wyjściowe NIE zawierają podciągu.

Zainstaluj bibliotekę, klonując bats-core/bats-assert do katalogu test/helpers/, a następnie ją wczytaj:

#!/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 i teardown: punkty zaczepienia cyklu życia testu

Bats udostępnia dwie specjalne funkcje — setup i teardown — które są automatycznie uruchamiane przed każdym testem i po nim. Służą one do przygotowania i posprzątania współdzielonego stanu, aby każdy test rozpoczynał się w znanym środowisku.

  • setup() jest uruchamiana przed każdym pojedynczym blokiem @test.
  • teardown() jest uruchamiana po każdym pojedynczym bloku @test, nawet jeśli test zakończy się niepowodzeniem.
  • Typowe zastosowania to tworzenie katalogów tymczasowych, ustawianie zmiennych środowiskowych oraz usuwanie plików tymczasowych po teście.
#!/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 i teardown_file: punkty zaczepienia na poziomie zestawu testów

Czasami kosztowne zasoby trzeba skonfigurować tylko raz na plik, a nie przed każdym testem. Bats udostępnia do tego celu funkcje setup_file i teardown_file.

  • setup_file() jest uruchamiana raz, przed wszystkimi testami w pliku.
  • teardown_file() jest uruchamiana raz, po wszystkich testach w pliku.
  • Użyj BATS_FILE_TMPDIR (dostępnej automatycznie), aby współdzielić dane między setup_file a testami — zwykłe zmienne nie zachowują wartości między podpowłokami.

Typowy przypadek użycia: uruchomienie serwera mock lub jednokrotne zbudowanie pliku binarnego, a następnie zakończenie jego działania na końcu:

#!/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"
}

Testowanie funkcji modyfikujących pliki

Bardzo często testuje się funkcje Bash, które odczytują system plików lub zapisują w nim dane. Kluczową techniką jest używanie katalogów tymczasowych (za pomocą mktemp -d w setup), dzięki czemu testy nigdy nie modyfikują rzeczywistych plików ani nie zakłócają się wzajemnie.

  • Zawsze pracuj wewnątrz $TEST_DIR (lub $BATS_TEST_TMPDIR — dostępnej automatycznie w nowszych wersjach Bats).
  • Używaj biblioteki pomocniczej bats-file do przejrzystych asercji dotyczących plików, takich jak assert_file_exists i assert_file_contains.
  • Nigdy nie wpisuj ścieżek na stałe, takich jak /tmp/myfile — równoległe uruchomienia testów będą ze sobą kolidować.
#!/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"
}

Mockowanie poleceń zewnętrznych

Funkcje często wywołują zewnętrzne programy, takie jak curl, aws lub git. W testach jednostkowych należy testować Państwa logikę, a nie rzeczywiste polecenie zewnętrzne. Najprostszą techniką mockowania w Bats jest zdefiniowanie w setup funkcji powłoki o takiej samej nazwie jak polecenie — ma ona pierwszeństwo przed rzeczywistym plikiem binarnym.

  • Zdefiniuj w setup funkcję taką jak curl() { echo 'mocked response'; return 0; } i wyeksportuj ją.
  • Użyj export -f curl, aby funkcja była widoczna w podpowłokach uruchamianych przez run.
  • W bardziej złożonych scenariuszach można również zapisać mock w pliku tymczasowym znajdującym się na ścieżce 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"
}

Pomijanie testów i tagowanie

Nie każdy test może być uruchamiany zawsze — czasami potrzebne jest rzeczywiste połączenie sieciowe, konkretne narzędzie lub określony system operacyjny. Bats udostępnia polecenie skip, które pozwala warunkowo pominąć test z informacyjnym komunikatem, zamiast komentować go lub przerywać działanie całego zestawu.

  • Wywołaj skip "reason" w dowolnym miejscu bloku @test, aby pominąć ten test.
  • Pominięte testy są oznaczane w danych wyjściowych jako S i nie są uznawane za nieudane.
  • Bats 1.5+ obsługuje tagi: oznaczaj testy za pomocą # bats test_tags=slow,network i filtruj je poleceniem 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/

Organizowanie kompletnego zestawu testów

Dobrze zorganizowany projekt Bats ma przewidywalny układ katalogów, dzięki czemu łatwo wdrażać nowych współtwórców i integrować projekt z potokami CI.

Zalecana struktura:

  • lib/ — produkcyjne funkcje Bash (jeden plik na dane zagadnienie: math.sh, fileutils.sh).
  • test/ — jeden plik .bats dla każdego pliku biblioteki (math.bats, fileutils.bats).
  • test/helpers/ — bats-assert, bats-file i bats-support jako podmoduły Git.
  • Makefile — cel test, aby współtwórcy mogli po prostu uruchomić make test.

Uruchom cały zestaw jednym poleceniem:

# 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

Sprawdzenie wiedzy: asercje Bats-core

Sprawdź, jak dobrze rozumiesz podstawowy mechanizm testowania w Bats-core.

Podsumowanie: testy jednostkowe Bash za pomocą Bats-core

W tej lekcji nauczyli się Państwo, jak organizować i pisać testy jednostkowe pojedynczych funkcji Bash za pomocą Bats-core. Oto najważniejsze informacje:

  • Struktura pliku testów — używaj shebangu #!/usr/bin/env bats oraz bloków @test z opisowymi nazwami.
  • load — wczytuj pliki bibliotek, aby funkcje były dostępne w testach bez kopiowania ich kodu.
  • run + $status + $output — podstawowy zestaw; zawsze używaj run do przechwytywania wyników bez natychmiastowego kończenia testu niepowodzeniem.
  • bats-assert — zamiast surowego [ ] wybieraj assert_success, assert_failure i assert_output, aby uzyskać czytelne komunikaty o niepowodzeniu.
  • setup / teardown — są uruchamiane przed każdym testem i po nim; setup_file / teardown_file są uruchamiane raz na plik.
  • Mockowanie — zastępuj polecenia zewnętrzne funkcjami powłoki o tych samych nazwach, eksportowanymi za pomocą export -f.
  • skip — warunkowo pomijaj testy zależne od niedostępnych zasobów.
  • Układ projektu — przechowuj katalogi lib/, test/ i test/helpers/ oddzielnie, aby ułatwić utrzymanie projektu i integrację z CI.

Te wzorce zapewniają projektom Bash taki sam rygor testowania, jaki stosuje się w każdym nowoczesnym projekcie programistycznym.

Często zadawane pytania

Czy lekcja „Testowanie jednostkowe funkcji za pomocą Bats-core” jest bezpłatna?

Tak — pełny tekst „Testowanie jednostkowe funkcji za pomocą Bats-core” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DevOps Bootcamp, przejdź na CoddyKit PRO. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.

Co nauczysz się w „Testowanie jednostkowe funkcji za pomocą Bats-core”?

Organizuj pliki testów, asercje oraz konfigurację i sprzątanie, aby weryfikować pojedyncze funkcje Bash. Ćwiczysz DevOps Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DevOps Bootcamp?

Nie wymagamy żadnego doświadczenia. DevOps Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.

Ile czasu zajmuje lekcja „Testowanie jednostkowe funkcji za pomocą Bats-core”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DevOps Bootcamp?

Tak. Każda lekcja DevOps Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Testowanie jednostkowe funkcji za pomocą Bats-core
  2. Mockowanie poleceń i zastępowanie narzędzi zewnętrznych
  3. Fixtures, środowiska tymczasowe i pokrycie kodu
  4. Uruchamianie testów powłoki w potokach CI
← Powrót do DevOps Bootcamp