Las pruebas automatizadas se utilizarán en tareas posteriores y también se utilizan ampliamente en proyectos reales.
Cuando escribimos una función, normalmente podemos imaginar qué debería hacer: qué parámetros dan qué resultados.
Durante el desarrollo, podemos verificar la función ejecutándola y comparando el resultado con el esperado. Por ejemplo, podemos hacerlo en la consola.
Si algo anda mal, arreglamos el código, lo ejecutamos nuevamente, verificamos el resultado y así sucesivamente hasta que funcione.
Pero esas “repeticiones” manuales son imperfectas.
Al probar un código mediante reejecuciones manuales, es fácil pasar por alto algo.
Por ejemplo, estamos creando una función f
. Escribí un código, probando: f(1)
funciona, pero f(2)
no funciona. Arreglamos el código y ahora f(2)
funciona. ¿Parece completo? Pero nos olvidamos de volver a probar f(1)
. Eso puede provocar un error.
Eso es muy típico. Cuando desarrollamos algo, tenemos en cuenta muchos casos de uso posibles. Pero es difícil esperar que un programador los revise todos manualmente después de cada cambio. Entonces resulta fácil arreglar una cosa y romper otra.
Las pruebas automatizadas significan que las pruebas se escriben por separado, además del código. Ejecutan nuestras funciones de varias maneras y comparan los resultados con los esperados.
Comencemos con una técnica llamada Behavior Driven Development o, en resumen, BDD.
BDD es tres cosas en una: pruebas Y documentación Y ejemplos.
Para comprender BDD, examinaremos un caso práctico de desarrollo.
Digamos que queremos crear una función pow(x, n)
que eleve x
a una potencia entera n
. Suponemos que n≥0
.
Esa tarea es sólo un ejemplo: existe el operador **
en JavaScript que puede hacer eso, pero aquí nos concentramos en el flujo de desarrollo que también se puede aplicar a tareas más complejas.
Antes de crear el código de pow
, podemos imaginar qué debería hacer la función y describirlo.
Dicha descripción se denomina especificación o, en resumen, especificación y contiene descripciones de casos de uso junto con pruebas para ellos, como esta:
describir("poder", función() { it("se eleva a la n-ésima potencia", function() { afirmar.equal(pow(2, 3), 8); }); });
Una especificación tiene tres bloques de construcción principales que puedes ver arriba:
describe("title", function() { ... })
¿Qué funcionalidad estamos describiendo? En nuestro caso estamos describiendo la función pow
. Se utiliza para agrupar a los “trabajadores”: it
bloquea.
it("use case description", function() { ... })
En el it
describimos de forma legible el caso de uso particular, y el segundo argumento es una función que lo prueba.
assert.equal(value1, value2)
El código dentro de it
bloque, si la implementación es correcta, debería ejecutarse sin errores.
Las funciones assert.*
se utilizan para comprobar si pow
funciona como se esperaba. Aquí estamos usando uno de ellos: assert.equal
, compara argumentos y arroja un error si no son iguales. Aquí comprueba que el resultado de pow(2, 3)
sea igual a 8
. Hay otros tipos de comparaciones y comprobaciones que agregaremos más adelante.
La especificación se puede ejecutar y ejecutará la prueba especificada en it
bloque. Eso lo veremos más tarde.
El flujo de desarrollo suele verse así:
Se escribe una especificación inicial, con pruebas para la funcionalidad más básica.
Se crea una implementación inicial.
Para comprobar si funciona, ejecutamos el marco de prueba Mocha (más detalles pronto) que ejecuta la especificación. Si bien la funcionalidad no está completa, se muestran errores. Hacemos correcciones hasta que todo funcione.
Ahora tenemos una implementación inicial funcional con pruebas.
Agregamos más casos de uso a la especificación, probablemente aún no admitidos por las implementaciones. Las pruebas empiezan a fallar.
Vaya a 3, actualice la implementación hasta que las pruebas no den errores.
Repita los pasos 3 a 6 hasta que la funcionalidad esté lista.
Entonces, el desarrollo es iterativo . Escribimos la especificación, la implementamos, nos aseguramos de que las pruebas pasen, luego escribimos más pruebas, nos aseguramos de que funcionen, etc. Al final, tenemos una implementación funcional y pruebas para ella.
Veamos este flujo de desarrollo en nuestro caso práctico.
El primer paso ya está completo: tenemos una especificación inicial para pow
. Ahora, antes de realizar la implementación, usemos algunas bibliotecas de JavaScript para ejecutar las pruebas, solo para ver que están funcionando (todas fallarán).
Aquí en el tutorial usaremos las siguientes bibliotecas de JavaScript para las pruebas:
Mocha: el marco central: proporciona funciones de prueba comunes que it
describe
y la función principal que ejecuta las pruebas.
Chai – la biblioteca con muchas afirmaciones. Permite usar muchas afirmaciones diferentes, por ahora solo necesitamos assert.equal
.
Sinon: una biblioteca para espiar funciones, emular funciones integradas y más, la necesitaremos mucho más adelante.
Estas bibliotecas son adecuadas para pruebas tanto en el navegador como en el servidor. Aquí consideraremos la variante del navegador.
La página HTML completa con estos marcos y especificaciones pow
:
<!DOCTYPE html> <html> <cabeza> <!-- agregue mocha css, para mostrar resultados --> <enlace rel="hoja de estilo" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- agregar código de marco mocha --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <guión> mocha.setup('bdd'); // configuración mínima </script> <!-- agregar chai --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <guión> // chai tiene muchas cosas, hagamos afirmar global let afirmar = chai.assert; </script> </cabeza> <cuerpo> <guión> función poder(x, n) { /* se va a escribir el código de función, vacío ahora */ } </script> <!-- el script con pruebas (describirlo...) --> <script src="test.js"></script> <!-- el elemento con id="mocha" contendrá los resultados de la prueba --> <div id="moca"></div> <!-- ejecutar pruebas! --> <guión> mocha.run(); </script> </cuerpo> </html>
La página se puede dividir en cinco partes:
El <head>
: agrega bibliotecas y estilos de terceros para las pruebas.
El <script>
con la función a probar, en nuestro caso – con el código para pow
.
Las pruebas: en nuestro caso, un script externo test.js
que tiene describe("pow", ...)
desde arriba.
Mocha utilizará el elemento HTML <div id="mocha">
para generar resultados.
Las pruebas se inician con el comando mocha.run()
.
El resultado:
A partir de ahora, la prueba falla, hay un error. Eso es lógico: tenemos un código de función vacío en pow
, por lo que pow(2,3)
devuelve undefined
en lugar de 8
.
Para el futuro, observemos que hay más ejecutores de pruebas de alto nivel, como karma y otros, que facilitan la ejecución automática de muchas pruebas diferentes.
Hagamos una implementación simple de pow
para que las pruebas pasen:
función poder(x, n) { devolver 8; // :) ¡hacemos trampa! }
¡Guau, ahora funciona!
Lo que hemos hecho es definitivamente una trampa. La función no funciona: un intento de calcular pow(3,4)
daría un resultado incorrecto, pero las pruebas pasan.
…Pero la situación es bastante típica, sucede en la práctica. Las pruebas pasan, pero la función funciona mal. Nuestra especificación es imperfecta. Necesitamos agregarle más casos de uso.
Agreguemos una prueba más para verificar que pow(3, 4) = 81
.
Podemos seleccionar una de dos formas de organizar la prueba aquí:
La primera variante: agregue una assert
más al mismo it
:
describir("poder", función() { it("se eleva a la n-ésima potencia", function() { afirmar.equal(pow(2, 3), 8); afirmar.equal(pow(3, 4), 81); }); });
El segundo – hacer dos pruebas:
describir("poder", función() { it("2 elevado a la potencia 3 es 8", function() { afirmar.equal(pow(2, 3), 8); }); it("3 elevado a la potencia 4 es 81", function() { afirmar.equal(pow(3, 4), 81); }); });
La principal diferencia es que cuando assert
desencadena un error, it
bloque termina inmediatamente. Entonces, en la primera variante, si la primera assert
falla, nunca veremos el resultado de la segunda assert
.
Hacer pruebas por separado es útil para obtener más información sobre lo que está sucediendo, por lo que la segunda variante es mejor.
Y además de eso, hay una regla más que es bueno seguir.
Una prueba comprueba una cosa.
Si miramos la prueba y vemos dos comprobaciones independientes en ella, es mejor dividirla en dos más simples.
Entonces sigamos con la segunda variante.
El resultado:
Como era de esperar, la segunda prueba falló. Claro, nuestra función siempre devuelve 8
, mientras que la assert
espera 81
.
Escribamos algo más real para que pasen las pruebas:
función poder(x, n) { dejar resultado = 1; para (sea i = 0; i < n; i++) { resultado *= x; } resultado de devolución; }
Para asegurarnos de que la función funcione bien, probémosla para obtener más valores. En lugar de it
bloques manualmente, podemos generarlos for
:
describir("poder", función() { función hacerPrueba(x) { let esperado = x * x * x; it(`${x} en la potencia 3 es ${expected}`, function() { afirmar.equal(pow(x, 3), esperado); }); } para (sea x = 1; x <= 5; x++) { hacerPrueba(x); } });
El resultado:
Vamos a agregar aún más pruebas. Pero antes de eso, observemos que las funciones auxiliares makeTest
y for
deben agruparse. No necesitaremos makeTest
en otras pruebas, solo es necesario en for
: su tarea común es verificar cómo pow
aumenta a la potencia dada.
La agrupación se realiza con una describe
anidada:
describir("poder", función() { describe("eleva x a la potencia 3", función() { función hacerPrueba(x) { let esperado = x * x * x; it(`${x} en la potencia 3 es ${expected}`, function() { afirmar.equal(pow(x, 3), esperado); }); } para (sea x = 1; x <= 5; x++) { hacerPrueba(x); } }); //...más pruebas a seguir aquí, ambas describen y se pueden agregar });
La describe
anidada define un nuevo "subgrupo" de pruebas. En el resultado podemos ver la sangría titulada:
En el futuro podremos agregar más describe
it
el nivel superior con sus propias funciones auxiliares, no verán makeTest
.
before/after
y beforeEach/afterEach
Podemos configurar funciones before/after
que se ejecutan antes/después de ejecutar las pruebas, y también funciones beforeEach/afterEach
que se ejecutan antes/después de cada it
.
Por ejemplo:
describir("prueba", función() { before(() => alert("Pruebas iniciadas – antes de todas las pruebas")); after(() => alert("Prueba finalizada – después de todas las pruebas")); beforeEach(() => alert("Antes de una prueba – ingrese una prueba")); afterEach(() => alert("Después de una prueba – salir de una prueba")); it('prueba 1', () => alerta(1)); it('prueba 2', () => alerta(2)); });
La secuencia de ejecución será:
Las pruebas comenzaron – antes de todas las pruebas (antes) Antes de una prueba: ingrese una prueba (antes de cada) 1 Después de una prueba: salir de una prueba (después de cada) Antes de una prueba: ingrese una prueba (antes de cada) 2 Después de una prueba: salir de una prueba (después de cada) Pruebas finalizadas – después de todas las pruebas (después)
Abra el ejemplo en la zona de pruebas.
Por lo general, beforeEach/afterEach
y before/after
se utilizan para realizar la inicialización, poner a cero contadores o hacer algo más entre las pruebas (o grupos de pruebas).
La funcionalidad básica de pow
está completa. Se realiza la primera iteración del desarrollo. Cuando terminemos de celebrar y beber champán, sigamos adelante y mejorémoslo.
Como se dijo, la función pow(x, n)
está destinada a funcionar con valores enteros positivos n
.
Para indicar un error matemático, las funciones de JavaScript suelen devolver NaN
. Hagamos lo mismo con valores no válidos de n
.
Primero agreguemos el comportamiento a la especificación (!):
describir("poder", función() { //... it("para n negativo el resultado es NaN", function() { afirmar.isNaN(pow(2, -1)); }); it("para n no entero el resultado es NaN", function() { afirmar.isNaN(pow(2, 1.5)); }); });
El resultado con nuevas pruebas:
Las pruebas recién agregadas fallan porque nuestra implementación no las admite. Así es como se hace BDD: primero escribimos pruebas fallidas y luego implementamos una implementación para ellas.
Otras afirmaciones
Tenga en cuenta la afirmación assert.isNaN
: comprueba si hay NaN
.
También hay otras afirmaciones en Chai, por ejemplo:
assert.equal(value1, value2)
: comprueba la igualdad value1 == value2
.
assert.strictEqual(value1, value2)
: comprueba la igualdad estricta value1 === value2
.
assert.notEqual
, assert.notStrictEqual
: comprobaciones inversas a las anteriores.
assert.isTrue(value)
– comprueba ese value === true
assert.isFalse(value)
– comprueba ese value === false
…la lista completa está en los documentos
Entonces deberíamos agregar un par de líneas a pow
:
función poder(x, n) { si (n < 0) devuelve NaN; si (Math.round(n)!= n) devuelve NaN; dejar resultado = 1; para (sea i = 0; i < n; i++) { resultado *= x; } resultado de devolución; }
Ahora funciona, pasan todas las pruebas:
Abra el ejemplo final completo en el sandbox.
En BDD, la especificación va primero, seguida de la implementación. Al final tenemos tanto la especificación como el código.
La especificación se puede utilizar de tres maneras:
Como Pruebas , garantizan que el código funciona correctamente.
Como Docs : los títulos describe
e it
qué hace la función.
Como ejemplos : las pruebas son en realidad ejemplos prácticos que muestran cómo se puede utilizar una función.
Con la especificación, podemos mejorar, cambiar e incluso reescribir de forma segura la función desde cero y asegurarnos de que siga funcionando correctamente.
Esto es especialmente importante en proyectos grandes cuando una función se usa en muchos lugares. Cuando cambiamos dicha función, simplemente no hay forma de verificar manualmente si cada lugar que la usa todavía funciona correctamente.
Sin pruebas, las personas tienen dos caminos:
Para realizar el cambio, pase lo que pase. Y luego nuestros usuarios encuentran errores, ya que probablemente no pudimos verificar algo manualmente.
O, si el castigo por los errores es severo, como no hay pruebas, la gente tiene miedo de modificar dichas funciones y entonces el código queda obsoleto y nadie quiere entrar en él. No es bueno para el desarrollo.
¡Las pruebas automáticas ayudan a evitar estos problemas!
Si el proyecto está cubierto de pruebas, simplemente no existe tal problema. Después de cualquier cambio, podemos ejecutar pruebas y ver muchas comprobaciones realizadas en cuestión de segundos.
Además, un código bien probado tiene una mejor arquitectura.
Naturalmente, esto se debe a que el código probado automáticamente es más fácil de modificar y mejorar. Pero también hay otra razón.
Para escribir pruebas, el código debe organizarse de tal manera que cada función tenga una tarea claramente descrita, entradas y salidas bien definidas. Eso significa una buena arquitectura desde el principio.
En la vida real esto a veces no es tan fácil. A veces es difícil escribir una especificación antes del código real, porque aún no está claro cómo debe comportarse. Pero, en general, escribir pruebas hace que el desarrollo sea más rápido y estable.
Más adelante en el tutorial encontrará muchas tareas con pruebas integradas. Así verás más ejemplos prácticos.
Escribir pruebas requiere buenos conocimientos de JavaScript. Pero apenas estamos empezando a aprenderlo. Entonces, para aclarar todo, a partir de ahora no es necesario que escriba pruebas, pero ya debería poder leerlas incluso si son un poco más complejas que en este capítulo.
importancia: 5
¿Qué pasa en la prueba de pow
a continuación?
it("Eleva x a la potencia n", function() { sea x = 5; dejar resultado = x; afirmar.equal(pow(x, 1), resultado); resultado *= x; afirmar.equal(pow(x, 2), resultado); resultado *= x; afirmar.equal(pow(x, 3), resultado); });
PD: Sintácticamente la prueba es correcta y pasa.
La prueba demuestra una de las tentaciones a las que se enfrenta un desarrollador al escribir pruebas.
Lo que tenemos aquí son en realidad 3 pruebas, pero presentadas como una única función con 3 afirmaciones.
A veces es más fácil escribir de esta manera, pero si ocurre un error, es mucho menos obvio qué salió mal.
Si ocurre un error en medio de un flujo de ejecución complejo, entonces tendremos que descubrir los datos en ese punto. De hecho, tendremos que depurar la prueba .
Sería mucho mejor dividir la prueba en it
bloques con entradas y salidas claramente escritas.
Como esto:
describe("Eleva x a la potencia n", función() { it("5 elevado a 1 es igual a 5", function() { afirmar.equal(pow(5, 1), 5); }); it("5 elevado a 2 es igual a 25", function() { afirmar.equal(pow(5, 2), 25); }); it("5 elevado a 3 es igual a 125", function() { afirmar.equal(pow(5, 3), 125); }); });
Reemplazamos el sencillo it
con describe
y un grupo de bloques it
. Ahora si algo falla veríamos claramente cuáles fueron los datos.
También podemos aislar una única prueba y ejecutarla en modo independiente it.only
en lugar de it
:
describe("Eleva x a la potencia n", función() { it("5 elevado a 1 es igual a 5", function() { afirmar.equal(pow(5, 1), 5); }); // Mocha ejecutará sólo este bloque it.only("5 elevado a 2 es igual a 25", function() { afirmar.equal(pow(5, 2), 25); }); it("5 elevado a 3 es igual a 125", function() { afirmar.equal(pow(5, 3), 125); }); });