bash tdd

Bash y desarrollo impulsado por pruebas



tdd (8)

Desde el punto de vista de la implementación, sugiero frameworks .

Desde un punto de vista práctico, sugiero no rendirme. Utilizo TDD en scripts bash y confirmo que vale la pena el esfuerzo.

Por supuesto, obtengo el doble de líneas de prueba que de código pero con scripts complejos, los esfuerzos de prueba son una buena inversión. Esto es cierto en particular cuando su cliente cambia de opinión al final del proyecto y modifica algunos requisitos. Tener un conjunto de pruebas de regresión es una gran ayuda para cambiar el código de bash complejo.

Al escribir más que un guión trivial en bash, a menudo me pregunto cómo hacer que el código sea comprobable.

Por lo general, es difícil escribir pruebas para el código bash, debido a que tiene pocas funciones que toman un valor y devuelven un valor, y alto en funciones que verifican y configuran algún aspecto en el entorno, modifican el sistema de archivos, invocar un programa, etc. - funciones que dependen del entorno o tienen efectos secundarios. Por lo tanto, el código de configuración y prueba se vuelve mucho más complicado que el código que prueban.

Por ejemplo, considere una función simple para probar:

function add_to_file() { local f=$1 cat >> $f sort -u $f -o $f }

El código de prueba para esta función puede consistir en:

add_to_file.antes de:

foo bar baz

add_to_file.after:

bar baz foo qux

Y código de prueba:

function test_add_to_file() { cp add_to_file.{before,tmp} add_to_file add_to_file.tmp cmp add_to_file.{tmp,after} && echo pass || echo fail rm add_to_file.tmp }

Aquí se prueban 5 líneas de código con 6 líneas de código de prueba y 7 líneas de datos.

Ahora considere un caso un poco más complicado:

function distribute() { local file=$1 ; shift local hosts=( "$@" ) for host in "${hosts[@]}" ; do rsync -ae ssh $file $host:$file done }

Ni siquiera puedo decir cómo comenzar a escribir una prueba para eso ...

Entonces, ¿hay una buena manera de hacer TDD en scripts bash, o debería darme por vencido y poner mis esfuerzos en otra parte?


Entonces esto es lo que aprendí:

  1. Sin embargo, hay algunos frameworks testing escritos en bash y para bash ...

  2. No es tanto que Bash no sea adecuado para TDD (aunque me vienen a la mente otros lenguajes que se ajustan mejor), sino las tareas típicas para las que se usa Bash (Instalación, Configuración del sistema), que son difíciles de escribir pruebas para , y particularmente difícil de configurar la prueba.

  3. El soporte deficiente de la estructura de datos en Bash hace que sea difícil separar la lógica de los efectos secundarios, y de hecho hay típicamente poca lógica en los scripts Bash. Eso hace que sea difícil dividir los guiones en fragmentos comprobables. Hay algunas funciones que se pueden probar, pero esa es la excepción, no la regla.

  4. La función es algo bueno (tm), pero solo pueden ir tan lejos.

  5. Las funciones anidadas pueden ser incluso mejores, pero también son limitadas.

  6. Al final del día, con un gran esfuerzo se puede obtener alguna cobertura, pero probará la parte menos interesante del código y mantendrá la mayor parte de las pruebas como una buena (o mala) prueba manual antigua.

Meta: decidí responder (y acepto) mi propia pregunta, porque no pude elegir entre las respuestas de Sinan Ünür (votado) y mouviciel''s (votado) que son igualmente útiles y perspicaces. Quiero destacar la respuesta de Stefano Borini , que aunque no me impresionó inicialmente, aprendí a apreciarlo con el tiempo. También sus patrones de diseño o mejores prácticas para la respuesta de scripts de shell (votados) referidos anteriormente fueron útiles.


Es posible que desee echar un vistazo a pepino / aruba. Hizo un buen trabajo para mí.

Además, puedes trozar todo lo que quieras haciendo algo como esto:

# # code.sh # some_function_calling_some_external_binary() { if ! external_binary action_1; then # ... fi if ! external_binary action_2; then # ... fi } # # test.sh # # now for the test, simply stub your external binary: external_binary() { if [ "$@" = "action_1" ]; then # stub action_1 elif [ "$@" = "action_2" ]; then # stub action_2 else external_binary $@ fi }


Escribir lo que Meszaros llama pruebas de consumo es difícil en cualquier idioma. Otro enfoque es verificar el comportamiento de comandos como rsync manualmente, luego escribir pruebas unitarias para probar funcionalidades específicas sin tocar la red. En este ejemplo ligeramente modificado, $ run se usa para imprimir los efectos secundarios si el script se ejecuta con la palabra clave "test"

function distribute { local file=$1 ; shift for host in $@ ; do $run rsync -ae ssh $file $host:$file done } if [[ $1 == "test" ]]; then run="echo" else distribute schedule.txt $* exit 0 fi # # Built-in self-tests # output=$(mktemp) expected=$(mktemp) set -e trap "rm $got $expected" EXIT distribute schedule.txt login1 login2 > $output cat << EOF > $expected rsync -ae ssh schedule.txt login1:schedule.txt rsync -ae ssh schedule.txt login2:schedule.txt EOF diff $output $expected echo -n ''.'' echo; echo "PASS"


La guía avanzada de scripts de bash tiene un ejemplo de una función afirmar, pero aquí hay una función de afirmación más simple y más flexible: simplemente use eval de $ * para probar cualquier condición.

assert() { if ! eval $* ; then echo echo "===== Assertion failed: /"$*/" =====" echo "File /"$0/", line:$LINENO line:${BASH_LINENO[*]}" echo line:$(caller 0) exit 99 fi } # e.g. USAGE: assert [[ $r == 42 ]] assert "((r==42))"

BASH_LINENO y builtin bash de llamada son específicos de shell bash.


Si codifica un programa bash lo suficientemente grande como para requerir TDD, está utilizando un lenguaje incorrecto.

Le sugiero que lea mi publicación anterior sobre las mejores prácticas en la programación de bash, probablemente encontrará algo útil para hacer que su programa bash sea comprobable, pero mi afirmación anterior se mantiene.

Patrones de diseño o mejores prácticas para scripts de shell


Si está escribiendo código al mismo tiempo con pruebas, intente utilizar funciones que no utilicen nada aparte de sus parámetros y no modifique el entorno. Es decir, si su función también podría ejecutarse en una subshell, entonces será fácil de probar. Toma algunos argumentos y produce algo para stdout, o para un archivo, o tal vez hace algo en el sistema, pero la persona que llama no siente efectos secundarios.

Sí, terminarás con una gran cadena de funciones transmitiendo alguna variable WORKING_DIR que bien podría ser global, pero esto es un inconveniente menor comparado con la tarea de rastrear qué lee y modifica cada función. La habilitación de pruebas unitarias también es una bonificación gratuita.

Intente minimizar los casos en que necesita resultados. Un pequeño abuso de subshell hará mucho por mantener las cosas bien separadas (a expensas del rendimiento).

En lugar de estructura lineal, donde se llaman las funciones, se establece un entorno, luego se llaman otras, todas en un solo nivel, se intenta obtener un árbol de llamadas con datos mínimos que regresen. Devolver cosas en bash es inconveniente si adoptas la abstinencia autoimpuesta de los vars globales ...


eche un vistazo al marco Outthentic : está diseñado para crear escenarios que ejecuten cualquier código Bash y luego analicen el estándar utilizando DSL formal, es bastante fácil construir cualquier conjunto de pruebas Tdd / blackbox sobre esta herramienta.