0Pricing
DevOps Bootcamp · Lección

Pruebas unitarias de funciones con Bats-core

Estructure archivos de prueba, aserciones y configuración y desmontaje para verificar funciones individuales de Bash.

Pruebas unitarias de funciones con Bats-core es una lección gratuita de DevOps Bootcamp en CoddyKit. Esta es la lección 1 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DevOps Bootcamp, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DevOps Bootcamp incluye 4 lecciones en total.

¿Qué es Bats-core y por qué usarlo?

Bats-core (Bash Automated Testing System) es el framework de facto para realizar pruebas unitarias en Bash. Permite escribir pruebas estructuradas y repetibles para sus funciones y scripts de shell, del mismo modo que usaría JUnit para Java o pytest para Python.

  • Cada prueba es un bloque @test con una descripción comprensible.
  • Las pruebas tienen éxito cuando cada comando que contienen devuelve el código de salida 0.
  • Las pruebas fallan ante el primer código de salida distinto de cero o cuando falla una aserción.
  • La salida es compatible con TAP, por lo que los sistemas de CI (GitHub Actions, Jenkins, GitLab CI) la interpretan de forma nativa.

Instálelo mediante su gestor de paquetes o clone el repositorio:

# 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

Su primer archivo de pruebas de Bats

Un archivo de pruebas de Bats tiene la extensión .bats y comienza con un shebang especial. El bloque de construcción principal es la directiva @test, seguida de una cadena de descripción y un bloque de comandos.

  • El shebang #!/usr/bin/env bats indica al shell cómo ejecutar el archivo.
  • Cada bloque @test es un caso de prueba independiente.
  • Puede ejecutar un solo archivo con bats my_tests.bats o un directorio completo con bats test/.

A continuación se muestra la estructura mínima de un archivo de pruebas de 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
}

Cargar la función que se va a probar con 'load'

En proyectos reales, las funciones de Bash se encuentran en archivos de biblioteca, no dentro del propio archivo de pruebas. Bats proporciona el helper load para cargar archivos externos relativos al directorio del archivo de pruebas.

  • load '../lib/math.sh' carga el archivo antes de ejecutar cada prueba.
  • Después de cargarlo, todas las funciones definidas en ese archivo están disponibles en sus bloques de prueba.
  • Mantenga las funciones de biblioteca en un directorio lib/ y las pruebas en un directorio test/ para mantener una separación clara.

Ejemplo de estructura del proyecto y de la prueba correspondiente:

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

Aserciones principales: run, $status, $output

El comando run es el núcleo de las pruebas con Bats. En lugar de ejecutar un comando directamente, envolverlo con run captura su código de salida y su salida sin hacer que la prueba falle de inmediato.

  • $status — contiene el código de salida del último comando run.
  • $output — contiene la salida stdout combinada del último comando run.
  • $lines — es un array en el que cada elemento corresponde a una línea de salida (${lines[0]}, ${lines[1]}, etc.).

Esto permite comprobar tanto los casos de éxito como los de error:

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

Usar bats-assert para crear aserciones expresivas

Las aserciones integradas [ ] funcionan, pero proporcionan mensajes de error poco útiles. La biblioteca auxiliar bats-assert ofrece funciones de aserción expresivas que muestran exactamente qué salió mal.

  • assert_success — comprueba que $status sea 0.
  • assert_failure — comprueba que $status sea distinto de cero.
  • assert_output — comprueba que $output sea igual a la cadena indicada.
  • assert_output --partial — comprueba que la salida contenga la subcadena.
  • refute_output --partial — comprueba que la salida NO contenga la subcadena.

Instálela clonando bats-core/bats-assert en una carpeta test/helpers/ y, después, cárguela:

#!/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 y teardown: hooks del ciclo de vida de las pruebas

Bats proporciona dos funciones especiales — setup y teardown — que se ejecutan automáticamente alrededor de cada prueba. Úselas para preparar y limpiar el estado compartido, de modo que cada prueba comience en un entorno conocido.

  • setup() se ejecuta antes de cada bloque @test individual.
  • teardown() se ejecuta después de cada bloque @test individual, incluso si la prueba falla.
  • Usos habituales: crear directorios temporales, establecer variables de entorno y eliminar archivos temporales después de la prueba.
#!/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 y teardown_file: hooks a nivel de suite

A veces solo necesita configurar recursos costosos una vez por archivo, no antes de cada prueba. Bats proporciona setup_file y teardown_file para este propósito.

  • setup_file() se ejecuta una vez antes de todas las pruebas del archivo.
  • teardown_file() se ejecuta una vez después de todas las pruebas del archivo.
  • Use BATS_FILE_TMPDIR (disponible automáticamente) para compartir datos entre setup_file y sus pruebas; las variables normales no persisten entre subshells.

Un caso de uso habitual es iniciar un servidor simulado o compilar un binario una sola vez y desmontarlo al final:

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

Probar funciones que modifican archivos

Un patrón muy habitual consiste en probar funciones de Bash que leen del sistema de archivos o escriben en él. La técnica clave es usar directorios temporales (mediante mktemp -d en setup) para que las pruebas nunca toquen archivos reales ni interfieran entre sí.

  • Trabaje siempre dentro de $TEST_DIR (o $BATS_TEST_TMPDIR, disponible automáticamente en las versiones recientes de Bats).
  • Use la biblioteca auxiliar bats-file para realizar aserciones claras sobre archivos, como assert_file_exists y assert_file_contains.
  • No codifique nunca rutas como /tmp/myfile; las ejecuciones de pruebas en paralelo entrarían en conflicto.
#!/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"
}

Simular comandos externos

Las funciones suelen llamar a programas externos como curl, aws o git. En las pruebas unitarias quiere probar su lógica, no el comando externo real. La técnica de simulación más sencilla en Bats consiste en definir una función de shell con el mismo nombre que el comando dentro de setup; esta tiene prioridad sobre el binario real.

  • Defina una función como curl() { echo 'mocked response'; return 0; } en setup y expórtela.
  • Use export -f curl para que la función esté disponible en los subshells que inicia run.
  • También puede escribir el mock en un archivo temporal incluido en PATH para escenarios más complejos.
#!/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"
}

Omitir y etiquetar pruebas

No todas las pruebas pueden ejecutarse siempre; a veces necesita una conexión de red real, una herramienta específica o un sistema operativo determinado. Bats proporciona skip para omitir condicionalmente una prueba con un mensaje informativo, en lugar de comentarla o romper la suite.

  • Llame a skip "reason" en cualquier lugar dentro de un bloque @test para omitir esa prueba.
  • Las pruebas omitidas aparecen en la salida como S y no cuentan como fallos.
  • Bats 1.5 y versiones posteriores admiten tags: anote las pruebas con # bats test_tags=slow,network y filtre 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/

Estructurar una suite de pruebas completa

Un proyecto de Bats bien organizado sigue una estructura de directorios predecible, lo que facilita la incorporación de nuevos colaboradores y la integración con los pipelines de CI.

Estructura recomendada:

  • lib/ — funciones de Bash de producción (un archivo por responsabilidad: math.sh, fileutils.sh).
  • test/ — un archivo .bats por cada archivo de biblioteca (math.bats, fileutils.bats).
  • test/helpers/ — bats-assert, bats-file y bats-support como submódulos de git.
  • Makefile — un objetivo test para que los colaboradores solo tengan que ejecutar make test.

Ejecute la suite completa 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

Comprobación de conocimientos: aserciones de Bats-core

Compruebe su comprensión del mecanismo principal de pruebas de Bats-core.

Recapitulación: pruebas unitarias de Bash con Bats-core

En esta lección ha aprendido a estructurar y escribir pruebas unitarias para funciones individuales de Bash mediante Bats-core. Estas son las ideas principales:

  • Estructura del archivo de pruebas — use el shebang #!/usr/bin/env bats y bloques @test con nombres descriptivos.
  • load — cargue sus archivos de biblioteca para que las funciones estén disponibles en las pruebas sin copiar y pegar código.
  • run + $status + $output — el trío fundamental; use siempre run para capturar los resultados sin provocar un fallo inmediato de la prueba.
  • bats-assert — prefiera assert_success, assert_failure y assert_output a [ ] sin abstracciones, para obtener mensajes de error fáciles de interpretar.
  • setup / teardown — se ejecutan antes y después de cada prueba; setup_file / teardown_file se ejecutan una vez por archivo.
  • Simulación — sustituya los comandos externos mediante funciones de shell con el mismo nombre, exportadas con export -f.
  • skip — omita condicionalmente las pruebas que dependen de recursos no disponibles.
  • Estructura del proyecto — mantenga lib/, test/ y test/helpers/ separados para facilitar el mantenimiento y la integración con CI.

Estos patrones proporcionan a sus proyectos de Bash la misma disciplina de pruebas que aplicaría a cualquier proyecto de software moderno.

Preguntas frecuentes

¿La lección «Pruebas unitarias de funciones con Bats-core» es gratis?

Sí — el texto completo de «Pruebas unitarias de funciones con Bats-core» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DevOps Bootcamp, actualiza a CoddyKit PRO. El curso de DevOps Bootcamp incluye 4 lecciones en total.

¿Qué aprenderé en «Pruebas unitarias de funciones con Bats-core»?

Estructure archivos de prueba, aserciones y configuración y desmontaje para verificar funciones individuales de Bash. Practicas DevOps Bootcamp con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar DevOps Bootcamp?

No se requiere experiencia previa. DevOps Bootcamp en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 1 de 4.

¿Cuánto tiempo toma la lección «Pruebas unitarias de funciones con Bats-core»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de DevOps Bootcamp?

Sí. Cada lección de DevOps Bootcamp incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Pruebas unitarias de funciones con Bats-core
  2. Simulación de comandos y sustitución de herramientas externas
  3. Fixtures, entornos temporales y cobertura
  4. Ejecución de pruebas de shell en pipelines de CI
← Volver a DevOps Bootcamp