0Pricing
DevOps Bootcamp · Lezione

Test unitari delle funzioni con Bats-core

Strutturi file di test, asserzioni e configurazione/chiusura per verificare singole funzioni Bash.

Test unitari delle funzioni con Bats-core è una lezione DevOps Bootcamp gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DevOps Bootcamp, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DevOps Bootcamp include 4 lezioni in totale.

Che cos'è Bats-core e perché usarlo

Bats-core (Bash Automated Testing System) è il framework di fatto per i test unitari in Bash. Consente di scrivere test strutturati e ripetibili per le proprie funzioni e gli script di shell, proprio come si userebbe JUnit per Java o pytest per Python.

  • Ogni test è un blocco @test con una descrizione comprensibile.
  • I test hanno esito positivo quando ogni comando al loro interno restituisce il codice di uscita 0.
  • I test hanno esito negativo al primo codice di uscita diverso da zero o quando un'asserzione non viene soddisfatta.
  • L'output è compatibile con TAP, quindi i sistemi CI (GitHub Actions, Jenkins, GitLab CI) lo comprendono nativamente.

Installarlo tramite il gestore di pacchetti oppure clonare il repository:

# 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

Il primo file di test Bats

Un file di test Bats ha estensione .bats e inizia con uno shebang speciale. L'elemento di base è la direttiva @test, seguita da una stringa descrittiva e da un blocco di comandi.

  • Lo shebang #!/usr/bin/env bats indica alla shell come eseguire il file.
  • Ogni blocco @test è un caso di test indipendente.
  • È possibile eseguire un singolo file con bats my_tests.bats oppure un'intera directory con bats test/.

Di seguito è riportata la struttura minima di un file di 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
}

Caricare la funzione da testare con 'load'

Nei progetti reali, le funzioni Bash risiedono in file di libreria, non direttamente nel file di test. Bats mette a disposizione l'helper load per caricare file esterni in relazione alla directory del file di test.

  • load '../lib/math.sh' carica il file prima dell'esecuzione di ogni test.
  • Dopo il caricamento, tutte le funzioni definite in quel file sono disponibili nei blocchi di test.
  • Per mantenere una separazione ordinata, è consigliabile tenere le funzioni della libreria in una directory lib/ e i test in una directory test/.

Esempio di struttura del progetto e del relativo 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" ]
}

Asserzioni fondamentali: run, $status, $output

Il comando run è il cuore dei test Bats. Invece di eseguire direttamente un comando, racchiuderlo in run ne cattura il codice di uscita e l'output senza causare l'immediato fallimento del test.

  • $status — contiene il codice di uscita dell'ultimo comando run.
  • $output — contiene lo stdout combinato dell'ultimo comando run.
  • $lines — è un array in cui ogni elemento corrisponde a una riga dell'output (${lines[0]}, ${lines[1]} e così via).

È così possibile verificare sia i casi di successo sia quelli di errore:

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

Usare bats-assert per asserzioni espressive

Le asserzioni integrate con [ ] funzionano, ma producono messaggi di errore poco utili. La libreria helper bats-assert fornisce funzioni di asserzione espressive che indicano con precisione cosa non ha funzionato.

  • assert_success — verifica che $status sia 0.
  • assert_failure — verifica che $status sia diverso da zero.
  • assert_output — verifica che $output corrisponda alla stringa specificata.
  • assert_output --partial — verifica che l'output contenga la sottostringa.
  • refute_output --partial — verifica che l'output NON contenga la sottostringa.

Installare la libreria clonando bats-core/bats-assert in una cartella test/helpers/, quindi caricarla:

#!/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 e teardown: hook del ciclo di vita dei test

Bats mette a disposizione due funzioni speciali — setup e teardown — che vengono eseguite automaticamente prima e dopo ogni test. Le si può usare per preparare e ripulire lo stato condiviso, in modo che ogni test inizi in un ambiente noto.

  • setup() viene eseguita prima di ogni singolo blocco @test.
  • teardown() viene eseguita dopo ogni singolo blocco @test, anche se il test fallisce.
  • Usi comuni: creare directory temporanee, impostare variabili d'ambiente e rimuovere i file temporanei dopo il 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 e teardown_file: hook a livello di suite

A volte è necessario configurare risorse costose una sola volta per file, non prima di ogni singolo test. Bats mette a disposizione setup_file e teardown_file a questo scopo.

  • setup_file() viene eseguita una volta prima di tutti i test del file.
  • teardown_file() viene eseguita una volta dopo tutti i test del file.
  • Usare BATS_FILE_TMPDIR (disponibile automaticamente) per condividere dati tra setup_file e i test: le variabili normali non persistono tra i subshell.

Caso d'uso tipico: avviare un mock server o compilare un binario una sola volta, quindi arrestarlo alla fine:

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

Testare funzioni che modificano i file

Un pattern molto comune consiste nel testare funzioni Bash che leggono dal filesystem o vi scrivono. La tecnica fondamentale è usare directory temporanee (tramite mktemp -d in setup), così i test non modificano mai file reali e non interferiscono tra loro.

  • Lavorare sempre all'interno di $TEST_DIR (o $BATS_TEST_TMPDIR, disponibile automaticamente nelle versioni recenti di Bats).
  • Usare la libreria helper bats-file per asserzioni sui file più leggibili, come assert_file_exists e assert_file_contains.
  • Non codificare mai percorsi come /tmp/myfile: le esecuzioni parallele dei test entrerebbero in conflitto.
#!/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"
}

Simulare comandi esterni

Le funzioni chiamano spesso programmi esterni come curl, aws o git. Nei test unitari si desidera testare la propria logica, non il comando esterno reale. La tecnica di mocking più semplice in Bats consiste nel definire in setup una funzione shell con lo stesso nome del comando: questa avrà la precedenza sul binario reale.

  • Definire in setup una funzione come curl() { echo 'mocked response'; return 0; } ed esportarla.
  • Usare export -f curl per rendere la funzione visibile nei subshell creati da run.
  • Per scenari più complessi è anche possibile scrivere il mock in un file temporaneo presente in 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"
}

Saltare i test e aggiungere tag

Non tutti i test possono essere sempre eseguiti: a volte servono una connessione di rete reale, uno strumento specifico o un determinato sistema operativo. Bats mette a disposizione skip per saltare condizionalmente un test con un messaggio informativo, invece di commentarlo o interrompere la suite.

  • Chiamare skip "reason" in qualsiasi punto all'interno di un blocco @test per saltare il test.
  • I test saltati vengono mostrati nell'output come S e non vengono conteggiati come errori.
  • Bats 1.5+ supporta i tag: annotare i test con # bats test_tags=slow,network e filtrarli con 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/

Strutturare una suite di test completa

Un progetto Bats ben organizzato segue una struttura di directory prevedibile, rendendo più semplice l'inserimento di nuovi collaboratori e l'integrazione con le pipeline CI.

Struttura consigliata:

  • lib/ — funzioni Bash di produzione (un file per ogni ambito: math.sh, fileutils.sh).
  • test/ — un file .bats per ogni file di libreria (math.bats, fileutils.bats).
  • test/helpers/ — bats-assert, bats-file e bats-support come sottomoduli Git.
  • Makefile — un target test, così i collaboratori devono solo eseguire make test.

Eseguire l'intera suite con un solo comando:

# 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

Verifica delle conoscenze: asserzioni di Bats-core

Verifichi la propria comprensione del meccanismo fondamentale di test di Bats-core.

Riepilogo: test unitari Bash con Bats-core

In questa lezione ha imparato a strutturare e scrivere test unitari per singole funzioni Bash usando Bats-core. Ecco i concetti fondamentali:

  • Struttura del file di test — usare lo shebang #!/usr/bin/env bats e blocchi @test con nomi descrittivi.
  • load — caricare i file di libreria per rendere disponibili le funzioni nei test senza copiarle.
  • run + $status + $output — il trio fondamentale; usare sempre run per acquisire i risultati senza causare l'immediato fallimento del test.
  • bats-assert — preferire assert_success, assert_failure e assert_output a [ ] non elaborato, per ottenere messaggi di errore più leggibili.
  • setup / teardown — vengono eseguite prima e dopo ogni test; setup_file / teardown_file vengono eseguite una volta per file.
  • Mocking — sostituire i comandi esterni con funzioni shell omonime, esportate con export -f.
  • skip — saltare condizionalmente i test che dipendono da risorse non disponibili.
  • Struttura del progetto — mantenere separate lib/, test/ e test/helpers/ per facilitare la manutenzione e l'integrazione CI.

Questi pattern conferiscono ai progetti Bash la stessa disciplina di testing che applicherebbe a qualsiasi moderno progetto software.

Domande Frequenti

La lezione «Test unitari delle funzioni con Bats-core» è gratuita?

Sì — il testo completo di «Test unitari delle funzioni con Bats-core» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DevOps Bootcamp, passa a CoddyKit PRO. Il corso DevOps Bootcamp include 4 lezioni in totale.

Cosa imparerò in «Test unitari delle funzioni con Bats-core»?

Strutturi file di test, asserzioni e configurazione/chiusura per verificare singole funzioni Bash. Eserciti DevOps Bootcamp con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DevOps Bootcamp?

Non è richiesta alcuna esperienza precedente. DevOps Bootcamp su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.

Quanto tempo richiede la lezione «Test unitari delle funzioni con Bats-core»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DevOps Bootcamp?

Sì. Ogni lezione DevOps Bootcamp include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Test unitari delle funzioni con Bats-core
  2. Simulare comandi e creare stub per strumenti esterni
  3. Fixture, ambienti temporanei e coverage
  4. Eseguire test shell nelle pipeline CI
← Torna a DevOps Bootcamp