¿Por qué la ejecución aleatoria es una oportunidad para mejorar las Pruebas Unitarias?

¿Por qué la ejecución aleatoria es una oportunidad para mejorar las Pruebas Unitarias?

Trabajadores sin red de seguridad

Las pruebas unitarias (Unit tests) de software representan la red de seguridad que nos permite manipular con confianza y velocidad el software de forma tal que podamos cumplir con las necesidades del negocio y las corrientes de Agilismo y DevOps. No obstante, estas también presentan una serie de antipatrones y uno de ellos está asociado al descuido de ejecutarlas en orden determinístico (preestablecido), creando dependencias ocultas entre ellas.

¿Alguna vez ha visto pasar una prueba de unidad de forma aislada, pero falla cuando se ejecuta en una suite? O viceversa, ¿Qué la ejecución en Suite resulte efectiva, pero falle cuando se ejecuta de forma aislada?

Aleatorizar el orden de las pruebas nos ayudará a eliminar los errores en el diseño de nuestras pruebas.

Comencemos por examinar un síntoma … ¿Alguna vez ha visto nombres como este en un conjunto de pruebas?

func test01RunThisTestFirst() { … }
func testCheckSomethingElse() { … }

Este es un truco sucio para hacer que la prueba superior se ejecute primero, antes que cualquier otra prueba en la suite, se basa en el hecho de que actualmente, las pruebas dentro de una suite se ejecutan en orden lexicográfico, así 01 viene al principio de ASCII, por lo que test01RunThisTestFirst se ejecutará primero.

¿Por qué recurrimos a estos trucos?

¿Qué nos impulsa a hacer esto en primer lugar? Esto sucede porque esa primera prueba produce algún tipo de efecto secundario que queremos que se aplique a las otras pruebas. Las pruebas unitarias no se ejecutan en un vacío total. Puede haber un estado mutable compartido oculto en:

  • una preocupación transversal, como la analítica
  • el sistema de archivos
  • Valores predeterminados de usuario
  • una base de datos local
  • una base de datos remota

Cuando se ejecuta una prueba, puede dejar efectos secundarios. Es fuerte la tentación de utilizar estos efectos secundarios como punto de partida para la próxima prueba.

¿Cómo nos afecta el orden de la prueba?

No se equivoque: agregar caracteres a los nombres de prueba para forzarlos a un orden particular es un truco. Una prueba no debería influir en otra prueba. Las pruebas unitarias deben seguir el principio FIRST:

  • Fast: rápidas.
  • Isolated: aisladas / independientes.
  • Repeatable: repetibles.
  • Self-validating: autovalidadas.
  • Timely: escritas “a tiempo”, junto al código, justo antes o después, dependiendo de la estrategia, por ejemplo, TDD.

Para el caso de este artículo, se hace énfasis en la R de Repetible. “No deben depender de ningún estado inicial asumido, no deben dejar ningún residuo que impida que se vuelvan a ejecutar”.

Esto es importante porque las pruebas no son un sistema fijo. Agregamos nuevas pruebas. Eliminamos pruebas que ya no aportan valor. Las ejecutamos individualmente. Es importante que los casos de prueba internos de una suite sigan siendo independientes para evitar problemas a medida que nuestras suites crecen y cambian.

Si las pruebas siempre se ejecutan en el mismo orden, testA viene antes que testB, este orden implícito de los casos de prueba significa que creamos dependencias entre las pruebas y ni siquiera notarlo. Por lo general, cuando nos encontramos con el problema del orden de ejecución implícito, generalmente no ha sido porque alguien nombró deliberadamente las pruebas para controlar su orden. En cambio, ha sido completamente por accidente. Y no encuentra el problema hasta que ejecuta una prueba aislada que siempre se ha ejecutado como parte de una suite. (O bien, la prueba funciona de forma aislada, por lo que la registra es necesario para otra prueba que no estamos ejecutando en ese momento y que podría fallar).

¿Cómo hago que mis pruebas se ejecuten aleatoriamente?

Al momento de aleatorizar las pruebas es primordial, en caso de que fallen, poder reproducir el orden de ejecución exacto para poder depurar la prueba. Para esto, el framework de pruebas debe poder entregarnos la semilla (seed) de aleatorización ejecutada y poder ingresarla de forma tal que sea repetible. Vamos a ver como hacerlo en varios frameworks populares.

NodeJS

Para NodeJS con Mocha, como framework de pruebas, debemos instalar el módulo “rocha” que aleatoriza las pruebas:

npm install rocha –save-dev

Para ejecutarlo, podemos crear un script en nuestro archivo package.json así:

"test": "nyc --reporter=lcov rocha test/*.test.js",

Para lo cual solo tendremos que ejecutar

npm run test

Si la cantidad de pruebas ejecutadas (efectivas o no) es inferior a la del comando original, significa que hay algún archivo mal nombrado, por ejemplo prueba.js en vez de prueba.test.js Este error también altera los indicadores del análisis estático de código mediante herramientas como SonarQube.

En caso de que fallen las pruebas, solo habrá que ejecutar de nuevo las pruebas, pues estas se ejecutarán en el orden exacto que fallo hasta que logremos depurarlo. Esto es gracias a que Rocha  crea un archivo “.rocha.json” que guarda la semilla (seed) de la ejecución; este archivo se eliminará cuando se ejecute una prueba efectiva.

AngularJS

Debemos modificar el archivo src/karma.conf.js así:

const karmaJasmineSeedReporter = require('./karma-jasmine-seed-reporter'); // 1. importing custom reporter

module.exports = function(config) {
  config.set({
	…
	plugins: [
      …
      karmaJasmineSeedReporter // 2. reporter register as karma plugin
     ],
…
client: {
      …
      jasmine: {
        random: true,  // 3. Randomize
        stopOnFailure: true,
        failFast: true,
        // seed: 94349 // 4. specific seed if needed
      },
      …
},
…
reporters: ["progress", "kjhtml", "jasmine-seed"],
…
)};

Luego deberá crear el archivo src/karma-jasmine-seed-reporter.js con el siguiente contenido:

const karmaJasmineSeedReporter = function(baseReporterDecorator) {
  baseReporterDecorator(this);

  this.onBrowserComplete = function(browser, result) {
    if (result.order && result.order.random && result.order.seed) {
      this.write('\n%s: Randomized with seed %s\n', browser.name, result.order.seed);
    }
  };

  this.onRunComplete = function() {
      // Empty function
  }
};

module.exports = {
  'reporter:jasmine-seed': ['type', karmaJasmineSeedReporter] // 1. 'jasmine-seed' is a name that can be referenced in karma.conf.js
};

Para ejecutar las pruebas solo es necesario correr

ng test

O crear en el archivo package.json un script así:

"test": "ng test",

Al finalizar la prueba, se nos mostrará la semilla (seed) necesaria para reproducir el orden exacto de ejecución de la prueba. Este valor se incluirá en el archivo src/karma.conf.js

Respuesta de ejecución de pruebas en AngularJS

Para ver instrucciones mas detalladas, diríjase a https://blog.pragmatists.com/flaky-test-in-angular-lesson-learned-3e094894913f

Framework JUnit

Para JUnit versión mayor o igual a 5.6 (jupiter) es necesario agregar al comienzo de la clase la siguiente anotación:

@TestMethodOrder(MethodOrderer.Random.class)

Si se ejecuta las pruebas con el IDE se puede ver que estas ahora aparecen en orden aleatorio y en la consola de ejecución aparece el seed o semilla. Este es un ejemplo de IntelliJ

Ejemplo respuesta ejecución de Pruebas Unitarias en IntelliJ

Al ejecutar las pruebas con Gradle o Maven, se podrá ver en consola la seeed o semilla:

Ejemplo resultado pruebas JUnit mediante Gradle

Proyectos administrados con Maven

Si no usa JUnit o la versión que tiene es antigua, Maven provee una forma para ejecutar las pruebas aleatoriamente, debe modificar su pom.xml de la siguiente forma

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>3.0.0-M3</version>
	...
	<configuration>
		...
		<runOrder>random</runOrder>
		...
	</configuration>
	...
</plugin>

Para ejecutar la prueba solo será necesario correr el comando:

mvn test

Si quisiera comprobar que las Unit Tests se están ejecutando aleatoriamente, deberá correr el siguiente comando:

mvn test -X | grep runOrder

ADVERTENCIA: Maven y su plugin Surefire aún no entregan información de la semilla (seed) de ejecución. Esta feature se encuentra en desarrollo, en el siguiente link puede seguir la implementación https://github.com/apache/maven-surefire/pull/112

Xcode

Xcode 10 agregó una nueva opción importante a XCTest: orden de prueba aleatorio. Puede habilitarlo editando su esquema. Consejo de teclado: en lugar de pasar el mouse al selector de esquema, hacer clic y seleccionar «Editar esquema …», simplemente presione ⌘ <

En el editor de esquemas, seleccione Prueba en la columna de la izquierda. Luego seleccione la pestaña Información.

Editar esquema - Prueba - Información

Para cada paquete de prueba, haga clic en el botón Opciones. A continuación, seleccione la opción «Orden de ejecución aleatoria».

Opciones: orden de ejecución aleatorio

Esto aleatorizará dos ordenamientos al ejecutar pruebas:

  • Suites de prueba
  • Casos de prueba dentro de cada suite

¡Hagamos esto para cada paquete de prueba dentro de cada esquema!

Pero Xcode se queda corto. Supongamos que nos hemos basado en el orden de prueba implícito sin saberlo. Existe la posibilidad de que al ejecutar las pruebas dentro de una suite en orden aleatorio se manifieste un problema oculto. Esté atento a las pruebas que fallan inesperadamente.

¿Pero entonces, qué? Con un diagnóstico cuidadoso, podríamos intentar encontrar una solución. Pero la próxima vez que hagamos las pruebas, estarán en un orden diferente. Esto podría enmascarar la falla. Entonces, ¿cómo sabemos si corregimos el problema? ¡Nosotros no! No, a menos que podamos volver a ejecutar las pruebas en el mismo orden.

RSpec nos da un ejemplo de cómo hacer esto correctamente . Cuando le dice a RSpec que ejecute pruebas en orden aleatorio, imprime la semilla que utilizó para su generador de números aleatorios. Luego, puede especificar esa misma semilla para obtener pruebas en el mismo orden.

Hasta que tengamos alguna forma de bloquear el orden aleatorio, ¡no sabremos si hemos eliminado los problemas que revela esta función!

La imposibilidad actual de volver a ejecutar las pruebas en el mismo orden evita que esta nueva característica sea tan útil como podría ser. 

Conclusión

La redacción y comprensión de las pruebas debería ser bastante fácil para los desarrolladores para que sean eficaces. La ejecución de pruebas aleatorias ayuda a los desarrolladores a crear pruebas más confiables y consistentes.

Referencias

Las principales referencias de este artículo son:

Imagen principal: Lunch atop a Skyscraper.

Author: Alexander Andrade

Ingeniero de Sistemas, MBA y Especialista en Gerencia de Proyectos Tel: +57-317-241-5118

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.