¿Cómo probar tus scripts Bash de forma efectiva?
Prueba scripts de Bash con modo estricto, rastreo, Bats, shUnit2, comandos simulados, directorios temporales, ShellCheck y automatización de CI.
Cómo Probar Tus Scripts de Bash de Manera Efectiva
Los scripts de Bash a menudo manipulan archivos, servicios, despliegues y datos de producción. Probar tus scripts de Bash de manera efectiva te ayuda a detectar suposiciones incorrectas antes de que un trabajo de limpieza elimine el directorio equivocado o un script de despliegue omita un comando fallido.
No necesitas un gran marco de trabajo para empezar. Combina opciones defensivas del shell, verificaciones estáticas, pruebas unitarias enfocadas y entornos de prueba temporales para que tus scripts fallen de manera ruidosa y predecible.
Fundamentos: Codificación Defensiva y Depuración
Antes de implementar pruebas unitarias formales, la primera capa de defensa contra errores reside en la estructura del script en sí. Utilizar configuraciones operativas estrictas puede ayudar a convertir errores sutiles en tiempo de ejecución en fallos inmediatos, facilitando su depuración.
Encabezado Defensivo Esencial
Muchos scripts de Bash de producción comienzan con opciones más estrictas:
#!/bin/bash
# Salir inmediatamente si un comando sale con un estado distinto de cero.
set -e
# Tratar las variables no definidas como un error al sustituirlas.
set -u
# Evitar que los errores en una tubería sean enmascarados.
set -o pipefail
Combinar estos en set -euo pipefail es común. Ten en cuenta que set -e tiene casos límite en condicionales, subcapas y tuberías, por lo que aún debes verificar explícitamente los fallos esperados en lugar de asumir que el modo estricto reemplaza las pruebas.
Depuración Manual con Rastreo
Para una depuración rápida o para entender el flujo de ejecución del script, Bash ofrece capacidades de rastreo integradas:
- Rastreo de Comandos (
-x): Imprime los comandos y sus argumentos a medida que se ejecutan, precedidos por+. - Sin Ejecución (
-n): Lee los comandos pero no los ejecuta (útil para verificar errores de sintaxis).
Puedes habilitar el rastreo al ejecutar el script o dentro del propio script:
# Ejecutar el script con rastreo
bash -x ./mi_script.sh
# Habilitar el rastreo dentro del script para una sección específica
echo "Iniciando operación compleja..."
set -x # Habilitar rastreo
llamada_funcion_compleja arg1 arg2
set +x # Deshabilitar rastreo
echo "Operación finalizada."
Adoptando Marcos de Pruebas Unitarias Formales
La depuración manual no es sostenible para lógica compleja. Los marcos de pruebas unitarias formales te permiten definir casos de prueba repetibles, afirmar resultados esperados y automatizar el proceso de validación.
1. Bats (Sistema de Pruebas Automatizadas de Bash)
Bats es, sin duda, el marco más popular y fácil para las pruebas de Bash. Te permite escribir pruebas usando sintaxis familiar de Bash, haciendo que las afirmaciones sean simples y legibles.
Características Clave de Bats:
- Las pruebas se escriben con sintaxis similar a Bash.
- Utiliza el comando simple
runpara ejecutar el script/función objetivo. - Proporciona variables de aserción integradas como
$status,$outputy$lines.
Ejemplo: Probando una Función Simple
Imagina que tienes un script (calculadora.sh) que contiene una función calcular_suma.
Fragmento de calculadora.sh:
calcular_suma() {
if [[ $# -ne 2 ]]; then
echo "Error: Se requieren dos argumentos" >&2
return 1
fi
echo $(( $1 + $2 ))
}
test/calculadora.bats:
#!/usr/bin/env bats
# Obtener el script que contiene las funciones a probar.
# BATS_TEST_DIRNAME apunta al directorio que contiene este archivo de prueba.
source "$BATS_TEST_DIRNAME/../calculadora.sh"
@test "Entradas válidas deben devolver la suma correcta" {
run calcular_suma 10 5
# Afirmar que la función devolvió un estado de éxito (0)
[ "$status" -eq 0 ]
# Afirmar que la salida coincide con la expectativa
[ "$output" = "15" ]
}
@test "Entradas faltantes deben devolver estado de error (1)" {
run calcular_suma 5
[ "$status" -ne 0 ]
[ "$status" -eq 1 ]
# En versiones recientes de bats-core, stderr está disponible cuando se usa `run`.
# [ "$stderr" = "Error: Se requieren dos argumentos" ]
}
Para ejecutar las pruebas:
bats test/calculadora.bats
2. ShUnit2
ShUnit2 sigue el estilo de pruebas xUnit, lo que lo hace familiar para los desarrolladores que vienen de lenguajes como Python o Java. Requiere obtener los archivos del marco y se adhiere a una convención de nomenclatura estricta (setUp, tearDown, test_...).
Características Clave de ShUnit2:
- Soporta rutinas de configuración y desmontaje para la limpieza.
- Proporciona un rico conjunto de funciones de aserción integradas (por ejemplo,
assertTrue,assertEquals).
Estructura de ShUnit2
#!/bin/bash
# Obtener shUnit2. Ajusta esta ruta para tu instalación.
. /usr/local/share/shunit2/shunit2
# Definir variables/fixtures
setUp() {
# Código a ejecutar antes de cada prueba
ARCHIVO_TEMP=$(mktemp)
}
tearDown() {
# Código a ejecutar después de cada prueba (limpieza)
rm -f "$ARCHIVO_TEMP"
}
test_suma_basica() {
local resultado
# Llamar a la función que se está probando
resultado=$(mi_funcion_script 1 2)
# Usar una función de aserción
assertEquals "3" "$resultado"
}
# Si tu paquete shUnit2 espera una obtención explícita al final,
# obténlo después de tus funciones de prueba en lugar de cerca del inicio.
Mejores Prácticas para Pruebas de Scripts Bash
Las pruebas efectivas van más allá de ejecutar un marco; requieren un aislamiento cuidadoso de los componentes y la gestión de dependencias ambientales.
1. Manejo de Entrada, Salida y Errores
Tus pruebas deben verificar los flujos estándar (stdout, stderr) y el código de salida final, que es el mecanismo principal para señalar éxito o fracaso en Bash.
- Códigos de Salida: Prueba
status -eq 0para éxito y valores distintos de cero para condiciones de error como fallo de análisis o archivos faltantes. - Salida Estándar (
stdout): Este es típicamente la salida de datos principal. Usa$outputde Bats o captura la salida en ShUnit2 para afirmar la corrección. - Error Estándar (
stderr): Los errores, advertencias y mensajes de depuración deben dirigirse aquí. Crucialmente, asegúrate de que los scripts de producción estén en silencio enstderrdurante las ejecuciones exitosas.
2. Aislamiento de Dependencias (Simulación)
Las pruebas unitarias deben probar tu código, no las herramientas del sistema externas (como curl, kubectl o git). Si tu script depende de un comando externo, debes simular ese comando durante las pruebas.
Método: Crea un directorio temporal que contenga archivos ejecutables simulados que tengan el mismo nombre que las dependencias reales. Antepón este directorio a tu $PATH antes de ejecutar la prueba, asegurando que tu script llame al simulacro en lugar de a la herramienta real.
Ejemplo de Simulación:
#!/bin/bash
# Archivo: /tmp/mock_bin/curl
if [[ "$1" == "--version" ]]; then
echo "Mock Curl 7.6"
exit 0
else
# Simular una respuesta API exitosa
echo '{"status": "ok"}'
exit 0
fi
En la configuración de tu prueba:
export PATH="/tmp/mock_bin:$PATH"
3. Pruebas de Integración con Entornos Temporales
Las pruebas de integración verifican que el script interactúe correctamente con el sistema de archivos y el sistema operativo. Usa directorios temporales para evitar contaminar el sistema o interferir con otras pruebas.
Usando mktemp
El comando mktemp -d crea un directorio temporal seguro y único. Debes realizar toda la manipulación de archivos (creación, modificación, limpieza) dentro de este directorio durante la ejecución de la prueba.
setUp() {
# Crear un directorio temporal para esta ejecución de prueba
TEST_ROOT=$(mktemp -d)
cd "$TEST_ROOT"
}
tearDown() {
# Limpiar el directorio temporal
cd - >/dev/null
rm -rf "$TEST_ROOT"
}
@test "El script debe crear el archivo de registro requerido" {
run mi_script_que_escribe_registros
# Afirmar que el archivo esperado existe en el directorio temporal
[ -f "./log/script.log" ]
}
4. Pruebas de Portabilidad
Las implementaciones de Bash varían ligeramente (por ejemplo, GNU Bash vs. macOS/BSD Bash). Si la portabilidad es una preocupación, ejecuta tu suite de pruebas en varios entornos objetivo (por ejemplo, usando contenedores Docker) para detectar diferencias sutiles en los comandos de utilidad o la expansión de parámetros.
Integrando las Pruebas en el Flujo de Trabajo
Las pruebas no deben ser una ocurrencia tardía. Incorpora tu suite de pruebas en tu control de versiones y en tu pipeline de CI/CD (Integración Continua/Despliegue Continuo).
- Control de Versiones: Almacena el directorio de pruebas (por ejemplo,
test/) junto con tus scripts fuente. - Hooks de Pre-Commit: Usa herramientas como
shellcheck(una herramienta de análisis estático) y formateadores para asegurar la calidad del código antes de los commits. - Automatización de CI: Configura tu servidor de CI (GitHub Actions, GitLab CI, Jenkins) para ejecutar la suite de pruebas de Bats o ShUnit2 automáticamente en cada push. Falla la compilación si alguna prueba devuelve un estado distinto de cero.
Advertencia: Las herramientas de análisis estático como
shellcheckson excelentes compañeras de las pruebas unitarias. Detectan errores comunes, problemas de portabilidad y vulnerabilidades de seguridad que las pruebas podrían pasar por alto. Siempre ejecutashellcheckcomo parte de tu rutina de pre-prueba.
Conclusión
Comienza con shellcheck y set -euo pipefail, luego agrega pruebas alrededor de las partes de tu script que analizan la entrada, eligen archivos, llaman a herramientas externas o realizan cambios irreversibles. Una pequeña suite de Bats con dependencias simuladas y directorios temporales suele ser suficiente para convertir un script arriesgado en una automatización que puedas cambiar con confianza.