Prueba de manejo Shell Scripts

Escribir shell scripts es muy parecido a la programación. Algunos scripts requieren poca inversión de tiempo; mientras que otros scripts complejos pueden requerir pensamiento, planificación y un compromiso mayor. Desde esta perspectiva, tiene sentido adoptar un enfoque basado en pruebas y una prueba unitaria de nuestros scripts de shell.

Para aprovechar al máximo este tutorial, debe estar familiarizado con la interfaz de línea de comandos (CLI); es posible que desee consultar el tutorial La línea de comando es su mejor amigo si necesita un repaso. También necesita una comprensión básica de los scripts de shell de tipo Bash. Finalmente, es posible que desee familiarizarse con los conceptos de desarrollo impulsado por pruebas (TDD) y las pruebas unitarias en general; Asegúrate de revisar estos tutoriales de prueba dirigidos por PHP para obtener la idea básica.


Preparar el entorno de programación

Primero, necesita un editor de texto para escribir sus scripts de shell y pruebas de unidad. Usa tu favorito!

Usaremos el marco de prueba de unidad de shell shUnit2 para ejecutar nuestras pruebas de unidad. Fue diseñado para, y funciona con, conchas similares a Bash. shUnit2 es un marco de código abierto publicado bajo la licencia GPL, y también se incluye una copia del marco con el código fuente de muestra de este tutorial..

Instalar shUnit2 es muy fácil; simplemente descargue y extraiga el archivo en cualquier ubicación de su disco duro. Está escrito en Bash y, como tal, el marco consta de solo archivos de script. Si planea usar shUnit2 con frecuencia, le recomiendo que lo coloque en una ubicación en su RUTA.


Escribiendo nuestra primera prueba

Para este tutorial, extraiga shUnit en un directorio con el mismo nombre en su Fuentes carpeta (ver el código adjunto a este tutorial). Crear un Pruebas carpeta dentro Fuentes y agregó un nuevo archivo de llamada firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### function testWeCanWriteTests () assertEquals "funciona" "funciona" ## Llamar y ejecutar todas las pruebas. "… /Shunit2-2.1.6/src/shunit2"

Que haga ejecutable su archivo de prueba..

$ cd __your_code_folder __ / Pruebas $ chmod + x firstTest.sh

Ahora puedes simplemente ejecutarlo y observar la salida:

 $ ./firstTest.sh testWeCanWriteTests Ran 1 test. DE ACUERDO

Dice que hicimos una prueba exitosa. Ahora, hagamos que la prueba falle; cambiar el assertEquals declaración para que las dos cadenas no sean las mismas y ejecute la prueba nuevamente:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: esperado: pero era Ran 1 prueba. FALLADO (fallas = 1)

Un juego de tenis

Escribe pruebas de aceptación al comienzo de un proyecto / función / historia cuando puede definir claramente un requisito específico.

Ahora que tenemos un entorno de prueba en funcionamiento, escribamos un script que lea un archivo, tome decisiones en función del contenido del archivo y envíe información a la pantalla..

El objetivo principal del guión es mostrar la puntuación de un juego de tenis entre dos jugadores. Nos concentraremos solo en mantener la puntuación de un solo juego; Todo lo demás depende de usted. Las reglas de puntuación son:

  • Al principio, cada jugador tiene un puntaje de cero, llamado "amor"
  • La primera, la segunda y la tercera bolas ganadas están marcadas como "quince", "treinta" y "cuarenta".
  • Si en "cuarenta" la puntuación es igual, se llama "deuce".
  • Después de esto, la puntuación se mantiene como "Ventaja" para el jugador que obtiene un punto más que el otro jugador..
  • Un jugador es el ganador si logra tener una ventaja de al menos dos puntos y gana al menos tres puntos (es decir, si alcanzó al menos "cuarenta").

Definición de entrada y salida

Nuestra aplicación leerá la puntuación de un archivo. Otro sistema empujará la información en este archivo. La primera línea de este archivo de datos contendrá los nombres de los jugadores. Cuando un jugador obtiene un punto, su nombre se escribe al final del archivo. Un archivo de puntuación típico se ve así:

 John - Michael John John Michael John Michael Michael John John

Puedes encontrar este contenido en el input.txt archivo en el Fuente carpeta.

La salida de nuestro programa escribe la puntuación en la pantalla una línea a la vez. La salida debe ser:

 John - Michael John: 15 - Michael: 0 John: 30 - Michael: 0 John: 30 - Michael: 15 John: 40 - Michael: 15 John: 40 - Michael: 30 Deuce John: Advantage John: Ganador

Esta salida también se puede encontrar en el output.txt expediente. Usaremos esta información para verificar si nuestro programa es correcto..


La prueba de aceptación

Escribe pruebas de aceptación al comienzo de un proyecto / característica / historia cuando puede definir claramente un requisito específico. En nuestro caso, esta prueba simplemente llama a nuestra secuencia de comandos que se creará próximamente con el nombre del archivo de entrada como parámetro, y espera que la salida sea idéntica a la del archivo escrito a mano de la sección anterior:

 #! / usr / bin / env sh ### acceptanceTest.sh ### function testItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue 'El resultado esperado difiere'. PS  ## Llama y ejecuta todas las pruebas. "… /Shunit2-2.1.6/src/shunit2"

Vamos a ejecutar nuestras pruebas en el Fuente / Pruebas carpeta; por lo tanto, discos compactos… nos lleva a la Fuente directorio. Entonces trata de correr tennisGamse.sh, que aún no existe. Entonces el dif comando comparará los dos archivos: ./output.txt es nuestra salida escrita a mano y ./resultados.txt contendrá el resultado de nuestro guión. Finalmente, afirmar la verdad comprueba el valor de salida de dif.

Pero por ahora, nuestra prueba devuelve el siguiente error:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: línea 7: tennisGame.sh: comando no encontrado diff: ./results.txt: No existe tal archivo o directorio. EVALUACIÓN: El resultado esperado difiere. Ran 1 prueba. FALLADO (fallas = 1)

Convirtamos esos errores en un error agradable creando un archivo vacío llamado tennisGame.sh y hacerlo ejecutable. Ahora, cuando ejecutamos nuestra prueba, no obtenemos un error:

 ./acceptanceTest.sh testItCanProvideAllTheScores 1,9d0 < John - Michael < John: 15 - Michael: 0 < John: 30 - Michael: 0 < John: 30 - Michael: 15 < John: 40 - Michael: 15 < John: 40 - Michael: 30 < Deuce < John: Advantage < John: Winner ASSERT:Expected output differs. Ran 1 test. FAILED (failures=1)

Implementación con TDD

Crea otro archivo llamado unitTests.sh para nuestras pruebas unitarias. No queremos ejecutar nuestro script para cada prueba; Solo queremos ejecutar las funciones que probamos. Entonces, haremos tennisGame.sh Ejecutar sólo las funciones que residirán en funciones.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source ... /functions.sh function testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael" ## Llamar y ejecutar todas las pruebas. "… /Shunit2-2.1.6/src/shunit2"

Nuestra primera prueba es simple. Intentamos recuperar el nombre del primer jugador cuando una línea contiene dos nombres separados por un guión. Esta prueba fallará porque todavía no tenemos una getFirstPlayerFrom función:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: línea 8: getFirstPlayerFrom: comando no encontrado shunit2: ERROR assertEquals () requiere dos o tres argumentos; 1 shunit2 dado: ERROR 1: Juan 2: 3: Ran 1 prueba. DE ACUERDO

La implementación para getFirstPlayerFromes muy simple. Es una expresión regular que se empuja a través de la sed mando:

 ### functions.sh ### function getFirstPlayerFrom () echo $ 1 | sed -e 's /-.*//'

Ahora la prueba pasa:

 $ ./unitTest.sh testItCanProvideFirstPlayersName Ran 1 test. DE ACUERDO

Vamos a escribir otra prueba para el nombre del segundo jugador:

 ### unitTest.sh ### […] function testItCanProvideSecondPlayersName () assertEquals 'Michael "getSecondPlayerFrom' John - Michael"

La falla:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: esperado: pero era Corrió 2 pruebas. FALLADO (fallas = 1)

Y ahora la función de implementación para hacerla pasar.

 ### functions.sh ### […] function getSecondPlayerFrom () echo $ 1 | sed -e 's /.*-//'

Ahora tenemos pruebas de paso:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Ran 2 pruebas. DE ACUERDO

Vamos a acelerar las cosas

A partir de este punto, escribiremos una prueba y la implementación, y explicaré solo lo que merece ser mencionado.

Probemos si tenemos un jugador con una sola puntuación. Agregó la siguiente prueba:

 function testItCanGetScoreForAPlayerWithOnlyOneWin () clasificaciones = $ 'John - Michael \ nJohn' assertEquals '1 "getScoreFor' John '" clasificaciones de $ "'

Y la solución:

 función getScoreFor () jugador = clasificaciones de $ 1 = $ 2 totalMatches = $ (echo "$ clasificaciones" | grep $ player | wc -l) echo $ (($ totalMatches-1))

Usamos algunos pantalones de fantasía que citamos para pasar la secuencia de nueva línea (\norte) dentro de un parámetro de cadena. Entonces usamos grep para encontrar las líneas que contienen el nombre del jugador y contarlas con baño. Finalmente, restamos uno del resultado para contrarrestar la presencia de la primera línea (contiene solo datos no relacionados con la puntuación).

Ahora estamos en la fase de refactorización de TDD..

Acabo de darme cuenta de que el código realmente funciona para más de un punto por jugador, y podemos refactorizar nuestras pruebas para reflejar esto. Cambie la función de prueba anterior a la siguiente:

 function testItCanGetScoreForAPlayer () clasificaciones = $ 'John - Michael \ nJohn \ nMichael \ nJohn' assertEquals '2 "getScoreFor' John '" clasificaciones de $ "'

Las pruebas aún pasan. Es hora de seguir adelante con nuestra lógica:

 function testItCanOutputScoreAsInTennisForFirstPoint () assertEquals 'John: 15 - Michael: 0' "'displayScore' John '1' Michael '0'"

Y la implementación:

 función displayScore () si ["$ 2" -eq '1']; entonces playerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Sólo compruebo el segundo parámetro. Esto parece que estoy engañando, pero es el código más simple para hacer que la prueba pase. Escribir otra prueba nos obliga a agregar más lógica, pero qué prueba debemos escribir a continuación?

Hay dos caminos que podemos tomar. Probar si el segundo jugador recibe un punto nos obliga a escribir otro Si declaración, pero sólo tenemos que añadir una más Declaración si elegimos probar el segundo punto del primer jugador. Esto último implica una implementación más fácil, así que intentemos que:

 function testItCanOutputScoreAsInTennisForSecondPointFirstPlayer () assertEquals 'John: 30 - Michael: 0' "'displayScore' John '2' Michael '0'"

Y la implementación:

 función displayScore () si ["$ 2" -eq '1']; entonces playerOneScore = "15" else playerOneScore = "30" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Esto todavía parece hacer trampa, pero funciona perfectamente. Continuando por el tercer punto:

 function testItCanOutputScoreAsInTennisForTHIRDPointFirstPlayer () assertEquals 'John: 40 - Michael: 0' "'displayScore' John '3' Michael '0'"

La implementación:

función displayScore () si ["$ 2" -eq '1']; entonces playerOneScore = "15" elif ["$ 2" -eq '2']; entonces playerOneScore = "30" else playerOneScore = "40" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Esta if-elif-else esta empezando a molestarme Quiero cambiarlo, pero primero reformulemos nuestras pruebas. Tenemos tres pruebas muy similares; así que escribámoslos en una sola prueba que haga tres afirmaciones:

 function testItCanOutputScoreWhenFirstPlayerWinsFirst3Points () assertEquals 'John: 15 - Michael: 0' "'displayScore' John '1' Michael '0'" assertEquals 'John: 30 - Michael: 0' "'displayScore' John '2' Michael '0' "assertEquals 'John: 40 - Michael: 0'" 'displayScore' John '3' Michael '0' "

Eso está mejor, y todavía pasa. Ahora, vamos a crear una prueba similar para el segundo jugador:

 function testItCanOutputScoreWhenSecondPlayerWinsFirst3Points () assertEquals 'John: 0 - Michael: 15' "'displayScore' John '0' Michael '1'" assertEquals 'John: 0 - Michael: 30' "'displayScore' John '0' Michael '2' "assertEquals 'John: 0 - Michael: 40'" 'displayScore' John '0' Michael '3' "

Ejecutando los resultados de esta prueba en salida interesante:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: esperado: pero era ASSERT: esperado: pero era ASSERT: esperado: pero era

Bueno, eso fue inesperado. Sabíamos que Michael tendría puntuaciones incorrectas. La sorpresa es Juan; él debería tener 0 no 40. Vamos a arreglar eso modificando primero el if-elif-else expresión:

 función displayScore () si ["$ 2" -eq '1']; entonces playerOneScore = "15" elif ["$ 2" -eq '2']; entonces playerOneScore = "30" elif ["$ 2" -eq '3']; entonces playerOneScore = "40" else playerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

los if-elif-else Ahora es más complejo, pero al menos arreglamos los puntajes de John:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: esperado: pero era ASSERT: esperado: pero era ASSERT: esperado: pero era

Ahora vamos a arreglar a Michael:

 función displayScore () echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" función convertToTennisScore () if ["$ 1" -eq '1']; entonces playerOneScore = "15" elif ["$ 1" -eq '2']; entonces playerOneScore = "30" elif ["$ 1" -eq '3']; luego playerOneScore = "40" else playerOneScore = $ 1 fi echo $ playerOneScore; 

¡Eso funcionó bien! Ahora es el momento de refactorizar a ese feo. if-elif-else expresión:

 función convertToTennisScore () declare -a scoreMap = ('0 "15" 30 "40') echo $ scoreMap [$ 1];

Los mapas de valor son maravillosos! Vayamos al caso "Deuce":

 function testItSayDeuceWhenPlayersAreEqualAndHaveEnoughPoinst () assertEquals 'Deuce' "'displayScore' John '3' Michael '3'"

Verificamos "Deuce" cuando todos los jugadores tienen al menos una puntuación de 40.

 función displayScore () si [$ 2-gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; luego haga eco en "Deuce" o "echo" $ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' "fi

Ahora probamos para la ventaja del primer jugador:

 function testItCanOutputAdvantageForFirstPlayer () assertEquals 'John: Advantage' "'displayScore' John '4' Michael '3'"

Y para hacerlo pasar:

 función displayScore () si [$ 2-gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; luego haga eco en el "Deuce" elif [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -gt $ 4]; luego haga eco en "$ 1: Ventaja"; si no, echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Hay que feo if-elif-else de nuevo, y tenemos mucha duplicación también. Todas nuestras pruebas pasan, así que vamos a refactorizar:

 función displayScore () si outOfRegularScore $ 2 $ 4; luego checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 más echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi función outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] return $?  function checkEquality () si [$ 1 - eq $ 2]; luego echo "Deuce" fi function checkFirstPlayerAdv () si [$ 2 -gt $ 3]; luego echo "$ 1: Ventaja" fi

Esto funcionará por ahora. Probemos la ventaja para el segundo jugador:

 function testItCanOutputAdvantageForSecondPlayer () assertEquals 'Michael: Advantage' "'displayScore' John '3' Michael '4'"

Y el código:

 función displayScore () si outOfRegularScore $ 2 $ 4; luego checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 3 $ 4 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi funciona checkAdvantage () si [$ 2 -gt $ 4]; luego haga eco de "$ 1: Ventaja" elif [$ 4 -gt $ 2]; luego echo "$ 3: Ventaja" fi

Esto funciona, pero tenemos alguna duplicación en el checkAdvantage función. Simplifiquémoslo y llamémoslo dos veces:

 función displayScore () si outOfRegularScore $ 2 $ 4; luego checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi función checkAdvantage () si [$ 2-gr $ 3]; luego echo "$ 1: Ventaja" fi

Esto es realmente mejor que nuestra solución anterior, y vuelve a la implementación original de este método. Pero ahora tenemos otro problema: me siento incómodo con el $ 1, $ 2, $ 3 y $ 4 variables Necesitan nombres significativos:

 función displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 si outOfRegularScore $ firstPlayerScore $ secondPlayerScore; luego checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore else echo "$ 1: 'convertToTennisScore $ 1' - $ 3: 'convertToTennisScore $ 4' 'fi función checkPadentidadpacoppealinapacienteidadpacopea.com ]; luego echo "$ 1: Ventaja" fi

Esto hace que nuestro código sea más largo, pero es significativamente más expresivo. me gusta.

Es hora de encontrar un ganador:

 function testItCanOutputWinnerForFirstPlayer () assertEquals 'John: Winner' "'displayScore' John '5' Michael '3'"

Solo tenemos que modificar el checkAdvantageFor función:

 función checkAdvantageFor () si [$ 2 -gt $ 3]; entonces si ['expr $ 2 - $ 3' -gt 1]; luego echo "$ 1: Ganador" o echo "$ 1: Ventaja" fi fi

¡Casi terminamos! Como nuestro último paso, escribiremos el código en tennisGame.sh Para hacer pasar la prueba de aceptación. Este será un código bastante simple:

 #! / usr / bin / env sh ### tennisGame.sh ###… /functions.sh playersLine = "head -n 1 $ 1" echo "$ playersLine" firstPlayer = "getFirstPlayerFrom" $ playersLine "" secondPlayer = "getSecondPlayerFrom" playersLine "" wholeScoreFileContent = "cat $ 1" totalNoOfLines = "echo" $ wholeScoreFileContent "| wc -l" para currentLine en 'seq 2 $ totalNoOfLines'. -n $ currentLine '") secondPlayerScore = $ (getScoreFor $ secondPlayer"' echo \ "$ wholeScoreFileContent \" | head -n $ currentLine '") displayScore $ firstPlayer $ firstPlayerScore $ secondPlayer $ secondPlayerScore

Leemos la primera línea para recuperar los nombres de los dos jugadores, y luego leemos el archivo gradualmente para calcular la puntuación..


Pensamientos finales

Los scripts de shell pueden crecer fácilmente desde unas pocas líneas de código hasta unos cientos de líneas. Cuando esto sucede, el mantenimiento se vuelve cada vez más difícil. El uso de TDD y las pruebas unitarias pueden ayudar enormemente a hacer que su script complejo sea más fácil de mantener, sin mencionar que lo obliga a construir sus scripts complejos de una manera más profesional.