preloader

Automatización de Pruebas con Mocha

Automatización de Pruebas con Mocha

En las próximas tareas, exploraremos cómo automatizar pruebas, una práctica esencial en proyectos reales.

¿Por Qué Necesitamos Pruebas?

Al crear una función, generalmente sabemos qué debería hacer: para ciertos parámetros, esperamos un resultado específico.

Durante el desarrollo, podemos probar la función ejecutándola y comparando el resultado con lo esperado, por ejemplo, en la consola.

Si algo falla, corregimos el código, lo ejecutamos nuevamente y verificamos el resultado, repitiendo este proceso hasta que funcione correctamente.

Sin embargo, las pruebas manuales tienen sus limitaciones.

Cuando probamos manualmente, es fácil pasar por alto algo.

Por ejemplo, imaginemos que estamos desarrollando una función f. Escribimos algo de código y probamos: f(1) funciona, pero f(2) no. Corregimos el código y ahora f(2) funciona. ¿Terminamos? No necesariamente, ya que podríamos haber olvidado volver a probar f(1), lo que podría introducir errores.

Este es un problema común. Durante el desarrollo, pensamos en muchos casos de uso posibles. Sin embargo, no es razonable esperar que un programador pruebe todos los casos manualmente después de cada cambio. Es fácil corregir un error y crear otro.

La automatización implica escribir código de prueba adicional al código principal. Estas pruebas ejecutan nuestras funciones de varias maneras y comparan los resultados con los esperados.

Desarrollo Guiado por Comportamiento (BDD)

Utilizaremos una técnica conocida como Desarrollo Guiado por Comportamiento (BDD).

BDD es una combinación de pruebas, documentación y ejemplos.

Para entender BDD, veremos un caso práctico:

Desarrollo de «pow»: Especificación

Supongamos que queremos crear una función pow(x, n) que eleve x a la potencia de un número entero n, asumiendo que n ≥ 0.

Esta tarea es solo un ejemplo; en JavaScript ya existe el operador ** que realiza esta operación. Queremos centrarnos en el flujo de desarrollo, aplicable a tareas más complejas.

Antes de escribir el código de pow, podemos imaginar lo que la función debería hacer y describirlo.

Esta descripción se llama especificación o «spec» y contiene descripciones de uso junto con las pruebas para verificarlas, como:

				
					describe("pow", function() {
  it("eleva a la n-ésima potencia", function() {
    assert.equal(pow(2, 3), 8);
  });
});

				
			

Una especificación tiene tres bloques principales:

  1. describe(«título», function() { … }): Descripción de la funcionalidad que estamos describiendo. En nuestro caso, la función pow. Se utiliza para agrupar bloques de trabajo: los bloques it.

  2. it(«título», function() { … }): Bloque it. En el título de it, describimos el caso de uso. El segundo argumento es la función que lo prueba.

  3. assert.equal(value1, value2): Comprobación. El código dentro del bloque it que, si la implementación es correcta, debe ejecutarse sin errores.

Las funciones assert.* se utilizan para verificar que pow funcione como esperamos. Aquí usamos una de ellas: assert.equal, que compara argumentos y produce un error si no son iguales. Arriba se está verificando que el resultado de pow(2, 3) sea 8. Hay otros tipos de comparaciones y verificaciones que veremos más adelante.

El Flujo de Desarrollo

El flujo de desarrollo es el siguiente:

  1. Se escribe una especificación inicial, con pruebas para la funcionalidad más básica.
  2. Se crea una implementación inicial.
  3. Para verificar que funciona, ejecutamos el framework de pruebas Mocha (detallado más adelante) que ejecuta la “spec”. Mostrará los errores mientras la funcionalidad no esté completa. Hacemos correcciones hasta que todo funcione.
  4. Ahora tenemos una implementación inicial con pruebas.
  5. Añadimos más casos de uso a la especificación, probablemente no soportados aún por la implementación. Las pruebas comienzan a fallar.
  6. Regresamos al paso 3, actualizando la implementación hasta que las pruebas no den errores.
  7. Repetimos los pasos 3-6 hasta que la funcionalidad esté lista.

De esta manera, el desarrollo es iterativo. Escribimos la especificación, la implementamos, nos aseguramos de que las pruebas pasen, luego escribimos más pruebas, y nuevamente verificamos que pasen, etc. Al final, tenemos una implementación funcional con pruebas que la verifican.

La Especificación en Acción

Usaremos las siguientes bibliotecas JavaScript para las pruebas en este tutorial:

  • Mocha: El framework principal que proporciona funciones para pruebas comunes como describe e it, y la función principal que ejecuta las pruebas.
  • Chai: Una biblioteca con muchas funciones de comprobación (assertions). Permite el uso de diferentes comprobaciones. Usaremos assert.equal por ahora.
  • Sinon: Una biblioteca para espiar funciones, emular funciones incorporadas al lenguaje, y más. La necesitaremos a menudo más adelante.

Estas bibliotecas son adecuadas tanto para pruebas en el navegador como en el lado del servidor. Aquí nos enfocaremos en el navegador.

La siguiente es una página HTML con estos frameworks y nuestra especificación de pow:

				
					<!DOCTYPE html>
<html>
<head>
  
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
   <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <script>mocha.setup('bdd')</script>  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <script>let assert=chai.assert</script> </head>
<body> <script>function pow(x,n){}</script>  <script src="test.js"></script> 
  <div id="mocha"></div>
   <script>mocha.run()</script> </body>
</html>

				
			

Esta página se puede dividir en cinco partes:

  1. El <head>: Importa bibliotecas de terceros y estilos para las pruebas.
  2. El <script> con la función a comprobar, en nuestro caso con el código de pow.
  3. Las pruebas, en nuestro caso un fichero externo test.js que contiene una sentencia describe("pow", ...) al inicio.
  4. El elemento HTML <div id="mocha"> utilizado para la salida de los resultados.
  5. Los tests se inician con el comando mocha.run().

El resultado:

Por ahora, la prueba falla. Esto es lógico: nuestra función pow está vacía, por lo que pow(2,3) devuelve undefined en lugar de 8.

Para más adelante, recuerda que hay avanzadas herramientas para ejecutar pruebas (test-runners), como Karma y otras. Así que generalmente no es un problema configurar muchas pruebas diferentes.

Implementación Inicial

Vamos a realizar una implementación simple de pow, apenas suficiente para pasar la prueba:

				
					function pow(x, n) {
  return 8; // :) ¡Estamos haciendo trampa!
}

				
			

¡Ahora funciona!

Mejorando la Especificación

Lo que hicimos es trampa. La función no funciona correctamente: si ejecutamos un cálculo diferente, como pow(3,4), obtenemos un resultado incorrecto, pero la prueba pasa.

Esta situación es común en la práctica. Las pruebas pasan, pero la función no funciona bien. Nuestra especificación está incompleta. Necesitamos añadir más casos de uso a la especificación.

Añadamos una prueba para verificar que pow(3,4) sea 81.

Podemos organizar la prueba de dos maneras:

  1. Añadir un assert en el mismo it:
				
					function pow(x, n) {
  return 8; // :) ¡Estamos haciendo trampa!
}

				
			

¡Ahora funciona!

Mejorando la Especificación

Lo que hicimos es trampa. La función no funciona correctamente: si ejecutamos un cálculo diferente, como pow(3,4), obtenemos un resultado incorrecto, pero la prueba pasa.

Esta situación es común en la práctica. Las pruebas pasan, pero la función no funciona bien. Nuestra especificación está incompleta. Necesitamos añadir más casos de uso a la especificación.

Añadamos una prueba para verificar que pow(3,4) sea 81.

Podemos organizar la prueba de dos maneras:

  1. Añadir un assert en el mismo it:
				
					describe("pow", function() {
  it("eleva a la n-ésima potencia", function() {
    assert.equal(pow(2, 3), 8);
    assert.equal(pow(3, 4), 81);
  });
});

				
			
  1. Hacer dos pruebas:
				
					describe("pow", function() {
  it("2 elevado a la potencia de 3 es 8", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3 elevado a la potencia de 4 es 81", function() {
    assert.equal(pow(3, 4), 81);
  });
});

				
			

Notemos que cuando assert lanza un error, el bloque it termina inmediatamente. Aquí vemos la diferencia principal: si en la primera forma el primer assert falla, nunca veremos el resultado del segundo assert.

Hacer las pruebas por separado es útil para obtener información sobre lo que está pasando, así que la segunda forma es mejor.

Además, tiene sentido poner más pruebas. Por ejemplo, añadamos una prueba para el caso base pow(1, 100):

				
					describe("pow", function() {
  it("2 elevado a la potencia de 3 es 8", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3 elevado a la potencia de 4 es 81", function() {
    assert.equal(pow(3, 4), 81);
  });

  it("1 elevado a cualquier potencia es 1", function() {
    assert.equal(pow(1, 100), 1);
  });
});

				
			

Hagamos una implementación simple:

				
					function pow(x, n) {
  if (n == 0) return 1;

  let result = x;

  for (let i = 1; i < n; i++) {
    result *= x;
  }

  return result;
}

				
			

Probémoslo:

Las pruebas pasan, lo que significa que todo funciona según lo especificado.

Agrupación de it

A veces, se puede agrupar los bloques it adicionales dentro de describe anidado, para agrupar casos de uso similares.

Por ejemplo, añadamos más pruebas para verificar casos adicionales de la función pow. Además de números naturales, queremos probar casos fraccionales como 1.5^3.

Para hacer esto, creamos un bloque describe("x elevado a n") adicional y movemos nuestras pruebas allí:

				
					describe("pow", function() {

  describe("eleva x a la potencia de n", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x} elevado a la potencia de 3 es ${expected}`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }

  });

  it("eleva 2 a la potencia de 3", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("eleva 3 a la potencia de 4", function() {
    assert.equal(pow(3, 4), 81);
  });

  it("eleva 1 a la potencia de cualquier número", function() {
    assert.equal(pow(1, 100), 1);
  });
});

				
			

De este modo, podemos añadir más pruebas fácilmente y organizar nuestro código de prueba de manera limpia y clara.

Related Post

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *