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
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
Al ejecutar las pruebas con Gradle o Maven, se podrá ver en consola la seeed o semilla:
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.
Para cada paquete de prueba, haga clic en el botón Opciones. A continuación, seleccione la opción «Orden de ejecución aleatoria».
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:
- Why Is Random Order a Big Deal for Test Quality?
- Flaky test in Angular — lesson learned
- Execute tests in random order
- Preventing Flaky Tests from Ruining your Test Suite
- Write better software tests with random test execution in mind
Imagen principal: Lunch atop a Skyscraper.
1 comentario en «¿Por qué la ejecución aleatoria es una oportunidad para mejorar las Pruebas Unitarias?»