0Pricing
DevOps Bootcamp · Lección

Fixtures, entornos temporales y cobertura

Cree fixtures de prueba aislados y mida qué ramas de los scripts ejercitan realmente sus pruebas.

Fixtures, entornos temporales y cobertura es una lección gratuita de DevOps Bootcamp en CoddyKit. Esta es la lección 3 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.

Por qué son importantes los fixtures y el aislamiento

Al probar scripts de Bash, el mayor riesgo son los efectos secundarios: que las pruebas modifiquen accidentalmente archivos reales, bases de datos reales o el estado real del sistema. Una prueba que funciona en su equipo pero corrompe datos de producción es peor que no tener ninguna prueba.

La solución son los fixtures de prueba: entornos controlados y desechables que reproducen las condiciones reales sin tocar nada real. Unos buenos fixtures le proporcionan:

  • Reproducibilidad: las pruebas producen el mismo resultado en cada ejecución
  • Aislamiento: las pruebas no interfieren entre sí ni con el host
  • Seguridad: las operaciones destructivas solo afectan a datos desechables
  • Velocidad: no hay llamadas de red ni operaciones de E/S pesadas salvo que sean absolutamente necesarias

En las pruebas de Bash, los fixtures suelen ser directorios temporales con archivos conocidos, ejecutables simulados colocados al principio de $PATH y variables de entorno limitadas al proceso de prueba.

Creación y limpieza de directorios temporales

El patrón estándar para crear un directorio temporal por prueba utiliza mktemp -d, que crea un directorio único en /tmp e imprime su ruta. Guarde la ruta y registre un trap para eliminarlo automáticamente cuando salga el shell, incluso si se produce un fallo.

Este modismo de dos líneas debería aparecer en cada archivo de pruebas que interactúe con el sistema de archivos:

#!/usr/bin/env bash
set -euo pipefail

# Create an isolated temp directory
TMPDIR=$(mktemp -d)
# Always clean up, even if the script exits early or errors out
trap 'rm -rf "$TMPDIR"' EXIT

echo "Working in: $TMPDIR"

# Simulate creating fixture files
mkdir -p "$TMPDIR/project/{src,tests,logs}"
echo 'version=1.2.3' > "$TMPDIR/project/.env"
echo 'Hello fixture' > "$TMPDIR/project/src/main.sh"

ls -R "$TMPDIR/project"
echo 'Temp dir will be removed automatically on exit'

Estructuración del árbol de directorios de un fixture

Un fixture bien estructurado refleja la disposición de directorios que el script que está probando espera realmente. Considérelo una raíz de proyecto falsa en miniatura. La función de configuración del fixture crea esta estructura antes de cada prueba y la función de limpieza la elimina.

Prácticas recomendadas:

  • Use una función setup() que su framework de pruebas llame antes de cada prueba
  • Use una función teardown() o cleanup() que se ejecute después de cada prueba, incluso si se produce un fallo
  • Mantenga los archivos del fixture mínimos: solo lo que el script lee realmente
  • Asigne nombres descriptivos a los archivos del fixture para diagnosticar fácilmente los fallos
#!/usr/bin/env bash
# fixture_helpers.bash — source this from your test files

FIXTURE_ROOT=''

setup_fixture() {
  FIXTURE_ROOT=$(mktemp -d)
  # Build the directory tree the deploy script expects
  mkdir -p "$FIXTURE_ROOT"/{dist,config,logs}
  echo '{"version":"2.0"}' > "$FIXTURE_ROOT/config/app.json"
  echo 'console.log("app")' > "$FIXTURE_ROOT/dist/index.js"
  touch "$FIXTURE_ROOT/logs/.gitkeep"
  export FIXTURE_ROOT
  echo "[setup] Fixture ready at $FIXTURE_ROOT"
}

teardown_fixture() {
  if [[ -n "$FIXTURE_ROOT" && -d "$FIXTURE_ROOT" ]]; then
    rm -rf "$FIXTURE_ROOT"
    echo '[teardown] Fixture removed'
  fi
}

# Self-test
setup_fixture
ls "$FIXTURE_ROOT"
teardown_fixture

Simulación de ejecutables con un PATH falso

Muchos scripts de Bash llaman a herramientas externas como curl, aws, docker o git. En las pruebas no querrá acceder a servicios reales, por lo que debe reemplazar esas herramientas por ejecutables falsos.

La técnica es sencilla:

  1. Cree un directorio temporal bin/ dentro de su fixture
  2. Escriba allí pequeños scripts de shell con los mismos nombres que las herramientas reales
  3. Anteponer ese directorio a $PATH antes de llamar al script que está probando

Como $PATH se busca de izquierda a derecha, se utiliza el ejecutable falso. El binario real nunca se invoca.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Create a fake 'curl' that records calls and returns canned data
FAKE_BIN="$FIXTURE/bin"
mkdir -p "$FAKE_BIN"

cat > "$FAKE_BIN/curl" << 'SCRIPT'
#!/usr/bin/env bash
# Log every argument for later inspection
echo "curl $*" >> "$FIXTURE_BIN_LOG"
# Return a canned HTTP 200 response body
echo '{"status":"ok","id":42}'
SCRIPT
chmod +x "$FAKE_BIN/curl"

# Export the log path so the fake can find it
export FIXTURE_BIN_LOG="$FIXTURE/curl_calls.log"

# Prepend fake bin directory to PATH
export PATH="$FAKE_BIN:$PATH"

# Now any call to 'curl' hits our fake
curl -s https://api.example.com/health
curl -X POST https://api.example.com/deploy

echo '--- Recorded curl calls ---'
cat "$FIXTURE_BIN_LOG"

Captura y comprobación de la salida de comandos

Un fixture solo resulta útil si puede comprobar lo que hizo su script. Los patrones estándar son:

  • Capturar stdout/stderr en variables con $() o sustitución de procesos
  • Inspeccionar los archivos de registro escritos por los binarios falsos
  • Comprobar explícitamente los códigos de salida con $? o lógica condicional
  • Verificar que determinados archivos se hayan creado, modificado o dejado intactos

Escribir pequeños helpers de comprobación enfocados hace que sus pruebas sean legibles y proporciona mensajes de error precisos cuando algo sale mal.

#!/usr/bin/env bash
set -euo pipefail

# Minimal assertion helpers
assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc"
    echo "  expected: $expected"
    echo "  actual:   $actual"
    return 1
  fi
}

assert_file_exists() {
  local desc="$1" file="$2"
  if [[ -f "$file" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — file not found: $file"
    return 1
  fi
}

assert_contains() {
  local desc="$1" needle="$2" haystack="$3"
  if [[ "$haystack" == *"$needle"* ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — '$needle' not found in output"
    return 1
  fi
}

# Demo usage
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'hello world' > "$TMPDIR/greeting.txt"
OUT=$(cat "$TMPDIR/greeting.txt")
assert_eq   'file content matches'     'hello world' "$OUT"
assert_file_exists 'greeting file created' "$TMPDIR/greeting.txt"
assert_contains    'output has hello'     'hello'       "$OUT"

Ámbito de las variables de entorno en las pruebas

Los scripts suelen leer variables de entorno como $HOME, $CONFIG_PATH o $DATABASE_URL. En las pruebas debe sobrescribirlas sin contaminar el entorno real.

El enfoque más seguro consiste en ejecutar el script que está probando en un subshell con solo las variables que establezca explícitamente. El comando env permite limpiar el entorno y volver a añadir únicamente lo que necesita:

  • env -i VAR=val ./script.sh: entorno completamente limpio
  • (export VAR=val; ./script.sh): el subshell hereda el entorno del proceso padre y añade sus sobrescrituras

El uso de subshells también significa que, si el script cambia $IFS, $PWD u otro estado global, esos cambios nunca regresan al ejecutor de pruebas.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Write a tiny script under test that reads env vars
cat > "$FIXTURE/deploy.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
ENV_NAME=${DEPLOY_ENV:-unknown}
CFG=${CONFIG_DIR:-/etc/app}
echo "Deploying to $ENV_NAME using config from $CFG"
SCRIPT
chmod +x "$FIXTURE/deploy.sh"

echo '--- Test 1: staging env ---'
# Run in a clean subshell with explicit vars
(
  export DEPLOY_ENV=staging
  export CONFIG_DIR="$FIXTURE/config"
  "$FIXTURE/deploy.sh"
)

echo '--- Test 2: production env ---'
(
  export DEPLOY_ENV=production
  export CONFIG_DIR=/etc/prod-config
  "$FIXTURE/deploy.sh"
)

echo '--- Test 3: defaults ---'
# No env vars set — script should use its own defaults
env -i PATH="$PATH" "$FIXTURE/deploy.sh"

Introducción a kcov para la cobertura de Bash

La cobertura responde a la pregunta: ¿qué líneas (y ramas) de mi script ejecutaron realmente las pruebas? Un porcentaje de cobertura alto no garantiza que el código sea correcto, pero uno bajo revela rutas no probadas que probablemente contengan errores.

La principal herramienta para medir la cobertura de Bash es kcov. Funciona instrumentando el script a nivel del sistema operativo mediante PTRACE (Linux) o dtrace (macOS), por lo que no requiere cambios en el código fuente. Genera un informe HTML que muestra en rojo las líneas no cubiertas y en verde las cubiertas.

Uso básico:

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • Abra coverage-out/index.html en un navegador para inspeccionar los resultados
  • En CI, analice coverage-out/myscript.sh/coverage.json para obtener un porcentaje legible por máquinas

Nota: kcov debe instalarse por separado (brew install kcov en macOS, apt install kcov en Ubuntu 20.04 o posterior).

Ejecución de kcov contra un script real

A continuación se muestra un ejemplo completo con un script listo para desplegar, una prueba que lo ejercita y la invocación de kcov que mide la cobertura. Observe que el directorio de salida es específico de cada prueba, para que más adelante pueda combinar los resultados de varias ejecuciones.

#!/usr/bin/env bash
# This demo shows the *structure* of a kcov workflow.
# It will not run kcov itself (not guaranteed to be installed),
# but the script under test and test runner are fully runnable.
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# 1. Script under test
cat > "$FIXTURE/process.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
INPUT="$1"
if [[ ! -f "$INPUT" ]]; then
  echo "ERROR: file not found" >&2
  exit 1
fi
LINE_COUNT=$(wc -l < "$INPUT")
if (( LINE_COUNT == 0 )); then
  echo "WARNING: file is empty"
else
  echo "Processed $LINE_COUNT lines"
fi
SCRIPT
chmod +x "$FIXTURE/process.sh"

# 2. Happy path test (exercises line 6 and 11)
echo -e 'one\ntwo\nthree' > "$FIXTURE/data.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/data.txt")
echo "Happy path: $OUT"

# 3. Empty file test (exercises line 9)
touch "$FIXTURE/empty.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/empty.txt")
echo "Empty file: $OUT"

# 4. Missing file test (exercises line 5-6)
if ! OUT=$("$FIXTURE/process.sh" "$FIXTURE/missing.txt" 2>&1); then
  echo "Missing file (expected error): $OUT"
fi

# To measure coverage, wrap each call with kcov:
# kcov --include-path="$FIXTURE" "$FIXTURE/cov/happy" "$FIXTURE/process.sh" ...
# kcov --merge "$FIXTURE/cov/all" "$FIXTURE/cov/happy" "$FIXTURE/cov/empty"

Combinación de la cobertura de varias ejecuciones de pruebas

Una sola prueba rara vez cubre todas las ramas. Ejecute varias pruebas, cada una en su propio directorio de salida de cobertura, y después combínelas. La opción --merge de kcov combina varias ejecuciones en un único informe unificado.

El patrón habitual en una canalización de CI:

  • Ejecute la prueba A → salida en cov/test_a/
  • Ejecute la prueba B → salida en cov/test_b/
  • Combine → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • Analice cov/all/<script>/coverage.json para obtener el porcentaje final

También puede imponer un umbral mínimo y hacer que la compilación de CI falle si la cobertura cae por debajo de este:

#!/usr/bin/env bash
# extract_coverage.sh — parse kcov JSON and fail below threshold
set -euo pipefail

MIN_COVERAGE=80   # percent
COV_JSON="${1:-coverage-out/myscript.sh/coverage.json}"

if [[ ! -f "$COV_JSON" ]]; then
  echo "ERROR: coverage JSON not found at $COV_JSON" >&2
  exit 1
fi

# kcov JSON contains a key like: "percent_covered": "87.50"
PERCENT=$(grep -oP '"percent_covered":\s*"\K[0-9.]+' "$COV_JSON")
PERCENT_INT=${PERCENT%%.*}   # truncate decimal

echo "Coverage: ${PERCENT}%  (minimum: ${MIN_COVERAGE}%)"

if (( PERCENT_INT < MIN_COVERAGE )); then
  echo "FAIL: coverage ${PERCENT}% is below threshold ${MIN_COVERAGE}%" >&2
  exit 1
fi

echo "PASS: coverage threshold met"

Cobertura de ramas frente a cobertura de líneas

Hay dos métricas principales de cobertura con las que se encontrará:

  • Cobertura de líneas: ¿se ejecutó esta línea? Es fácil de falsear: una sola prueba puede recorrer muchas líneas y omitir rutas condicionales importantes.
  • Cobertura de ramas: ¿se tomó cada rama de cada if, case y &&/||? Es un indicador mucho más sólido. Requiere pruebas tanto para el lado verdadero como para el falso de cada decisión.

kcov informa de ambas. La idea clave es que una cobertura de líneas del 100 % no implica una cobertura de ramas del 100 %. Considere este script: una sola prueba con un archivo no vacío cubrirá todas las líneas, pero nunca se alcanzará la rama del archivo vacío (la línea 9 de abajo):

#!/usr/bin/env bash
# Illustrates line vs branch coverage gap
set -euo pipefail

check_file() {
  local f="$1"
  if [[ -f "$f" ]]; then        # branch A (true) OR branch B (false)
    local lines
    lines=$(wc -l < "$f")
    if (( lines > 0 )); then    # branch C (true) OR branch D (false)
      echo "File has $lines lines"
    else
      echo "File is empty"     # branch D — unreached if only tested with non-empty file
    fi
  else
    echo "File missing"        # branch B — unreached if only tested with existing file
  fi
}

# Only one test: covers lines 5-10 (4 of 6 branches)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'data' > "$TMPDIR/sample.txt"
check_file "$TMPDIR/sample.txt"

# To reach 100% branch coverage you also need:
# check_file "/nonexistent/path"
# check_file "$TMPDIR/empty.txt" (after: touch "$TMPDIR/empty.txt")

Integración de fixtures y cobertura en CI

Para integrarlo todo: una canalización de CI sólida para proyectos de Bash combina la configuración de fixtures, la ejecución de pruebas con kcov, la combinación de resultados y una comprobación del umbral en un único script. Este script se convierte en el punto de entrada de CI: un comando para ejecutarlo todo.

Principios de diseño del ejecutor de pruebas de CI:

  • Cada caso de prueba llama a setup_fixture y registra teardown_fixture mediante trap
  • El script sometido a prueba se invoca con un $PATH ficticio y variables de entorno con ámbito limitado
  • kcov envuelve cada invocación y escribe los resultados en un subdirectorio numerado
  • Después de todas las pruebas, kcov combina los resultados y el script del umbral determina si la compilación puede continuar
  • El ejecutor de CI termina con un código distinto de cero si alguna prueba o la comprobación de cobertura falla
#!/usr/bin/env bash
# ci_runner.sh — full fixture + coverage pipeline entry point
set -euo pipefail

SRC="./src/deploy.sh"
COV_ROOT="$(mktemp -d)/coverage"
trap 'rm -rf "$COV_ROOT"' EXIT
mkdir -p "$COV_ROOT"

PASS=0
FAIL=0
RUN_NUM=0

run_test() {
  local name="$1" test_fn="$2"
  local fixture
  fixture=$(mktemp -d)
  local cov_out="$COV_ROOT/run_$((++RUN_NUM))"

  if (
    trap 'rm -rf "$fixture"' EXIT
    export FIXTURE="$fixture"
    # Fake bin directory shadowing real tools
    mkdir -p "$fixture/bin"
    export PATH="$fixture/bin:$PATH"
    "$test_fn" "$fixture"
  ); then
    echo "PASS: $name"
    (( PASS++ )) || true
  else
    echo "FAIL: $name"
    (( FAIL++ )) || true
  fi
}

# Example test function
test_happy_path() {
  local fx="$1"
  mkdir -p "$fx/dist"
  echo 'app.js' > "$fx/dist/index.js"
  # Would normally run: kcov "$cov_out" "$SRC" --env=staging "$fx"
  echo "[test] happy path executed in $fx"
}

run_test 'happy_path' test_happy_path

echo "Results: $PASS passed, $FAIL failed"
(( FAIL == 0 ))

Comprobación de conocimientos: técnica del PATH ficticio

Compruebe su comprensión de la técnica del PATH ficticio utilizada en los fixtures de pruebas de Bash.

Recapitulación: fixtures, entornos temporales y cobertura

En esta lección se ha cubierto el conjunto completo de herramientas para realizar pruebas de Bash confiables y aisladas:

  • Directorios temporales — mktemp -d junto con trap ... EXIT garantiza la limpieza automática, independientemente de cómo termine la prueba.
  • Estructura de los fixtures — un par setup_fixture / teardown_fixture crea y elimina un árbol de directorios mínimo que refleja las entradas reales de los scripts.
  • PATH ficticio — coloque ejecutables simulados en $FIXTURE/bin/ y antepóngalo a $PATH para interceptar llamadas a curl, aws, docker o cualquier herramienta externa sin tocar los binarios del sistema.
  • Ámbito de las variables de entorno — ejecute el script sometido a prueba en un subshell (() o env -i) para que las variables modificadas nunca se filtren al ejecutor de pruebas.
  • Aserciones — pequeñas funciones auxiliares (assert_eq, assert_file_exists, assert_contains) producen una salida clara de aprobado o fallido y mensajes de error informativos.
  • Cobertura con kcov — envuelve la ejecución de scripts sin modificar el código fuente y genera informes HTML y JSON de cobertura de líneas y ramas.
  • Combinación y umbral — combine varias ejecuciones de kcov con --merge, analice el JSON y haga que CI falle si la cobertura cae por debajo del mínimo establecido.
  • Cobertura de ramas frente a cobertura de líneas — establezca siempre como objetivo la cobertura de ramas; la cobertura de líneas por sí sola puede omitir rutas condicionales completas y generar una falsa sensación de confianza.

Con estas técnicas, sus pruebas de Bash serán tan rigurosas como las pruebas de cualquier lenguaje compilado.

Preguntas frecuentes

¿La lección «Fixtures, entornos temporales y cobertura» es gratis?

Sí — el texto completo de «Fixtures, entornos temporales y cobertura» 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 «Fixtures, entornos temporales y cobertura»?

Cree fixtures de prueba aislados y mida qué ramas de los scripts ejercitan realmente sus pruebas. 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 3 de 4.

¿Cuánto tiempo toma la lección «Fixtures, entornos temporales y cobertura»?

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