Desde el círculo de Calidad en Paradigma insistimos mucho en que la cobertura de test unitarios de nuestros proyectos sea lo más alta posible, pero es evidente que cantidad (de test, en este caso) no siempre implica calidad, y la calidad es algo que nos importa.

Por suerte para nosotros, tenemos a nuestra disposición un tipo de test que hace precisamente eso, comprobar que nuestros test unitarios sean de buena calidad. Es un test de los test. No serviría de nada que nuestro código tuviera una cobertura del cien por cien en test unitarios, si estos no son capaces de detectar y prevenir problemas en nuestro software.

Test de test

La herramienta que testea los test unitarios son los test de mutaciones. Su funcionamiento es relativamente sencillo: la herramienta que estamos utilizando genera pequeños cambios en el ćodigo, conocidos como mutantes, y a continuación pasa los test unitarios.

Si los test unitarios fallan, es que han sido capaces de detectar ese cambio de código, y el mutante es eliminado. Si, por el contrario, los test unitarios pasan, el mutante sobrevive y la fiabilidad del test queda en entredicho.

Finalmente, los test de mutaciones presentan un porcentaje mutantes eliminados: cuanto más se acerque este porcentaje al 100%, mayor será la calidad de nuestros test unitarios.

Configuración

Uno de los aspectos más importantes en los test de mutaciones es su configuración. No todos las herramientas nos permiten el mismo nivel de configuración, pero hay que aprovechar esto lo máximo posible, para evitar que la ejecución de los test conlleve un tiempo innecesariamente más largo de lo normal.

Aunque lo más conservador y seguro puede parecer aplicar todos los tipos de mutaciones disponibles, esto va a hacer que se creen muchísimos mutantes, alargando el tiempo de ejecución de los test. Resulta más conveniente elegir bien los tipos de mutantes que vayan a hacer aflorar las partes más sensibles de nuestro código.

De todas maneras, aunque afináramos la configuración al máximo, cuantas más líneas de código y más test tenga nuestro proyecto, más tiempo de ejecución van a llevar los test de mutación.

Por eso, no conviene añadirlos a la batería de test que se pasan automáticamente al hacer una merge request o antes de mergear una rama, ya que ralentizarían mucho el desarrollo. Una de las opciones es programar una ejecución periódica de los test de mutación, que se vaya revisando para monitorizar la calidad de los test unitarios. Otra de las opciones puede ser lanzarlos en la ejecución automática, pero en paralelo.

Existen diversas herramientas según el lenguaje de programación que se esté usando. En este post veremos dos distintos: Pitest para Java y Stryker-mutator para Javascript.

Pitest

Pitest es una herramienta de código abierto para realizar test de mutaciones en Java. Está muy bien documentada, ya que su web dispone de una explicación sobre los propios test de mutaciones, conceptos básicos y varios quickstarts según la herramienta (Maven, Gradle, Ant o la propia línea de comandos) que se esté utilizando. En este post utilizaremos Maven.

Se añade Pitest como plugin en el POM del proyecto:

<plugin>
    <groupid>org.pitest</groupid>
    <artifactid>pitest-maven</artifactid>
    <version>LATEST</version>
</plugin>

Con esto ya sería suficiente para poder lanzar los test de mutaciones, sin ningún tipo de configuración, y se generaría un reporte en html, en la carpeta target/pit-reports/YYYMMDDHHMI. Se lanza directamente desde la línea de comandos:

$ mvn org.pitest:pitest-maven:mutationCoverage

Sin embargo, ya hemos dicho que la gracia es poder personalizar un poco esta configuración, para adaptarla a nuestras necesidades.

Por ejemplo, se pueden añadir otros formatos para el reporte que se saca. En este caso, se ha añadido el formato xml.

<plugin>
  <groupid>org.pitest</groupid>
  <artifactid>pitest-maven</artifactid>
  <version>1.4.9</version>
  <configuration>
     <outputformats>
        <outputformat>HTML</outputformat>
        <outputformat>XML</outputformat>
     </outputformats>
     <timestampedreports>false</timestampedreports>
  </configuration>
</plugin>

También se pueden excluir clases para que no se hagan mutantes en ellas:

<plugin>
  <groupid>org.pitest</groupid>
  <artifactid>pitest-maven</artifactid>
  <version>1.4.9</version>
  <configuration>
     <excludedclasses>
        <excludedclass>com.paradigma.example.configuration.*</excludedclass>
        <excludedclass>com.paradigma.example.dto.*</excludedclass>
     </excludedclasses>
  </configuration>
</plugin>

De la misma manera, se pueden incluir clases específicas. Pitest decide automáticamente cuáles tiene que mutar, pero se le puede indicar explícitamente con la etiqueta <targetClasses>.

Los mutantes que queremos que genere se indican bajo la etiqueta <mutators>. En su documentación, Pitest explica qué es lo que hace cada uno de ellos:

<plugin>
  <groupid>org.pitest</groupid>
  <artifactid>pitest-maven</artifactid>
  <version>1.4.9</version>
  <configuration>
     <mutators>
        <mutator>DEFAULTS</mutator>
        <mutator>EMPTY_RETURNS</mutator>
        <mutator>FALSE_RETURNS</mutator>
        <mutator>NULL_RETURNS</mutator>
        <mutator>REMOVE_CONDITIONALS</mutator>
        <mutator>TRUE_RETURNS</mutator>
        <mutator>AOR</mutator>
        <mutator>AOD</mutator>
     </mutators>
  </configuration>
</plugin>

Estas son solo algunas de las opciones de configuración que ofrece Pitest. Se pueden también especificar la cantidad de hilos que se quieren utilizar para correr estos test, número máximo de mutaciones a incluir en una unidad de análisis, si se quiere sacar la cobertura, etc.

Aquí hay un ejemplo del reporte que saca Pitest en formato HTML:

Se puede ver tanto la cobertura de test como el porcentaje de mutaciones eliminadas. Se puede pinchar en cada una de las carpetas para ver el porcentaje de cada una de las clases, y dentro de una clase para ver el código y ver a qué partes afectan las mutaciones no eliminadas:

Stryker-mutator

Stryker-mutator, también de código abierto, se diseñó originalmente para Javascript y amigos, como dice su web, aunque han sacado recientemente versión para C# y Scala. En esa web ofrecen también una detallada explicación sobre en qué consiste el test de mutaciones y su utilidad.

Como curiosidad, el nombre de esta herramienta viene de uno de los villanos de X-Men, William Stryker, cuyo único objetivo en la vida es matar mutantes.

En la documentación se incluye una guía para poder incluir Striker-mutator en el proyecto Javascript, como paquete de npm:

$ npm install -g stryker-cli

Una vez instalado Stryker, ejecutamos el comando que nos va a ayudar a crear el archivo stryker.conf.js, que es donde vamos a configurar nuestras opciones:

$ stryker init

Se nos irá preguntando qué framework utilizamos (Angular, Vue, React u otros), qué tipo de gestor (npm o yarn) preferimos, qué lenguaje estamos utilizando, cuál es el framework de test unitarios, etc. Con todo esto sacará ese archivo stryker.conf.js, donde se resume toda la configuración:

// This config was generated using a preset.
// Please see the handbook for more information: https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/vuejs.md#vuejs
module.exports = function(config) {
  config.set({
    mutate: ["src/**/*.js", "src/**/*.ts", "src/**/*.vue", "!src/**/*.spec.js"],
    mutator: "vue",
    testRunner: "jest",
    jest: {
      config: require('./test/unit/jest.conf.js')
    },
    reporters: ["progress", "clear-text", "html"],
    coverageAnalysis: "off"
  });
};

Como se puede ver, aquí no hay tantas opciones como en Pitest, pero algo se puede hacer.

El parámetro “mutate” nos pide que indiquemos la ruta de los archivos que queremos analizar. Si lo que queremos es excluir algún archivo, se añade el path con un ! delante. Conviene hacer esto con los archivos que contengan los test unitarios, ya que Stryker incluye todo por defecto, y no queremos mutar el código de los test.

También se puede configurar el formato de los reportes, el framework tanto de trabajo como de testing que estamos utilizando y si queremos un análisis de cobertura.

El reporte en HTML es muy parecido a Pitest, con el porcentaje global de mutaciones y su desglose por carpetas y archivos:

De nuevo, se puede llegar a inspeccionar cada uno de los archivos a nivel de código, para ver a qué partes afectan las mutaciones eliminadas y supervivientes:

Hay que tener en cuenta que alcanzar el 100% de mutaciones eliminadas es prácticamente imposible, pero tenemos que tender a que sea lo más alto posible.

Conclusiones

Pues hasta aquí este breve repaso sobre los test de mutaciones. El tema es muy amplio (integración con Sonarqube y Jenkins, por ejemplo), pero por lo menos ahora tenemos ya una visión general sobre esta herramienta.

En conclusión, los test de mutaciones son útiles para medir la calidad de nuestros test unitarios, comprobar que estos son efectivos y que no los hemos metido con el único objetivo de aumentar la cobertura de nuestro proyecto. Las herramientas para hacerlo son muchas y fáciles de implementar, y nos ayudan a mejorar la manera de hacer los test unitarios, que están en la base de la pirámide de testing.

Cuéntanos qué te parece.

Enviar.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.