Mutation testing en kotlin

0
834

Kotlin se está convirtiendo en un lenguaje cada vez más popular.
A medida que va ganando popularidad y su ecosistema crece, es interesante ver el grado de integración de herramientas conocidas. En este articulo vamos a probar en que estado está el mutation testing con PIT para tener una herramienta más de testing en nuestra caja de herramientas en este lenguaje.

Índice de contenidos

1. Introducción

Mi incansable lucha por entrar en la academia del profesor Charles Xavier y el aumento de mi uso de kotlin me han llevado a investigar que tal se llevan los tests de mutación con este lenguaje.

En un artículo previo hablamos de extreme mutation testing y en otro de otros compañeros de Mutation Testing con PIT por lo que aquí iremos más a la parte técnica de como usarlo con Kotlin para dar esa primera toma de contacto y animar a usarlo en los proyectos, por lo que si se echa de menos detalle, se recomienda la lectura de los artículos anteriores.

En este artículo también usaremos el ejemplo anterior de la calculadora que es bastante ilustrativo.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Lenovo t480 (1.80 GHz intel i7-8550U, 32GB DDR4)
  • Sistema Operativo: Linux Mint 20.2 Uma base: Ubuntu 20.04 focal Kernel: 5.13.0-27-generic x86_64 bits
  • Entorno de desarrollo: IntelliJ IDEA 2021.3.2 (Ultimate Edition).
  • Apache Maven 3.8.4
  • OpenJDK 64-Bit Server VM Temurin-17.0.1+12 (build 17.0.1+12, mixed mode, sharing)

3. Mutation testing

Mutation testing nace de la inquietud de saber si nuestros tests son correctos o no. Normalmente se usa la métrica de cobertura de código, esto es, si el test ha pasado por una determinada línea o no, pero eso no nos garantiza que los casos de prueba sean los más adecuados.

Mutation testing cambia el código del programa (mutaciones) para generar mutantes de tal forma que si los tests pasan se dice que el mutante sobrevive y si los tests no pasan, se dice que el mutante muere. El objetivo es que los mutantes mueran, ya que si cambiamos el comportamiento del código, pero las pruebas siguen pasando, es que no son del todo buenas. Se basa en dos hipótesis:

  • Hipótesis del programador competente: La mayoría de los errores introducidos son errores de sintaxis. Es interesante mencionar que esta hipótesis sigue siendo un área de investigación[1] y que de momento parece ser que se cumple, pero que faltarían operadores de mutación para que mutation testing diera todavía mejores resultados.
  • Hipótesis del efecto de acoplamiento: Pequeños fallos acoplados pueden juntarse y hacer que salgan otros defectos mayores.

Mutation testing parece que nos ayuda a mejorar la calidad de nuestros tests:

Nuestros resultados muestran que los desarrolladores que trabajan en proyectos con mutation testing escriben más pruebas de media durante más tiempo, en comparación con los proyectos que solo consideran la cobertura de código.
Los mutantes ayudan a hacer tests más efectivos: los desarrolladores expuestos a los mutantes escriben tests más efectivos en respuesta a ellos

[2]

Por lo que parece que además de estar vigente, puede ayudarnos, por eso buscar su ayuda en diferentes lenguajes de programación parece una buena idea.

4. Incluyendo mutation testing en kotlin

Vamos a ir viendo las clases que hemos creado y como añadir PIT a nuestro proyecto kotlin.

4.1. Clase calculadora

Tenemos nuestra clase calculadora

Calculator.kt
package org.example

class Calculator {
    fun addition(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber + secondNumber
    }

    fun subtraction(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber - secondNumber
    }

    fun multiplication(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber * secondNumber
    }

    fun division(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber / secondNumber
    }
}

A la que por supuesto le hemos puesto tests

CalculatorTest.kt
package org.example

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class CalculatorTest {
    private val sut = Calculator()

    @Test
    fun addition_should_add_two_numbers() {
        assertEquals(4, sut.addition(2, 2))
    }

    @Test
    fun subtraction_should_subtract_second_number_from_first_number() {
        assertEquals(6, sut.subtraction(8, 2))
    }

    @Test
    fun multiplication_should_multiply_two_numbers() {
        assertEquals(1, sut.multiplication(1, 1))
    }

    @Test
    fun division_should_divide_first_number_with_the_second() {
        assertEquals(5, sut.division(40, 8))
    }
}

En este apartado es importante destacar que aunque no es necesario poner el paquete (package org.example) para que funcione el programa, si es necesario ponerlo para que funcione PIT, si no lo ponemos obtenemos:

	[ERROR] Failed to execute goal org.pitest:pitest-maven:1.7.4:mutationCoverage (run-pitest) on project mutatationTesting: Execution run-pitest of goal org.pitest:pitest-maven:1.7.4:mutationCoverage failed: No mutations found. This probably means there is an issue with either the supplied classpath or filters.

Los tests están en junit5, con las siguientes dependencias se ejecutan correctamente

        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test-junit5</artifactId>
            <version>1.6.10</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>

Antes de seguir vemos como todos los tests pasan

Todos los tests pasan

Y tenemos un 100% de cobertura

Tenemos el 100% de cobertura

4.2. Añadiendo PIT

Ahora, lo que tenemos que hacer es añadir a nuestro pom las dependencias necesarias

       
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>

Ademas del plugin de PIT

            <plugin>
                <groupId>org.pitest</groupId>
                <artifactId>pitest-maven</artifactId>
                <version>${pitest.maven.version}</version>
                <configuration>
                </configuration>
                <executions>
                    <execution>
                        <id>run-pitest</id>
                        <phase>test</phase>
                        <goals>
                            <goal>mutationCoverage</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.pitest</groupId>
                        <artifactId>pitest-junit5-plugin</artifactId>
                        <version>${pitest.junit5.plugin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

Quedando el pom.xml asi:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>


    <groupId>org.example</groupId>
    <artifactId>mutatationTesting</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

    <name>consoleApp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <kotlin.code.style>official</kotlin.code.style>
        <kotlin.compiler.jvmTarget>17</kotlin.compiler.jvmTarget>
        <pitest.maven.version>1.7.4</pitest.maven.version>
        <pitest.junit5.plugin.version>0.15</pitest.junit5.plugin.version>
    </properties>

    <repositories>
        <repository>
            <id>mavenCentral</id>
            <url>https://repo1.maven.org/maven2/</url>
        </repository>
    </repositories>

    <build>
        <sourceDirectory>src/main/kotlin</sourceDirectory>
        <testSourceDirectory>src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>1.6.10</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <configuration>
                    <mainClass>MainKt</mainClass>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.pitest</groupId>
                <artifactId>pitest-maven</artifactId>
                <version>${pitest.maven.version}</version>
                <configuration>
                </configuration>
                <executions>
                    <execution>
                        <id>run-pitest</id>
                        <phase>test</phase>
                        <goals>
                            <goal>mutationCoverage</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.pitest</groupId>
                        <artifactId>pitest-junit5-plugin</artifactId>
                        <version>${pitest.junit5.plugin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test-junit5</artifactId>
            <version>1.6.10</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>1.6.10</version>
        </dependency>
    </dependencies>

</project>

Si ejecutamos:

mvn test && mvn org.pitest:pitest-maven:mutationCoverage

Podemos ir a target/pit-reports/[FECHA] y vemos el index.html con la información.

Desde la consola también se nos da información:

================================================================================
- Timings
================================================================================
> pre-scan for mutations : < 1 second
> scan classpath : < 1 second
> coverage and dependency analysis : < 1 second
> build mutation tests : < 1 second
> run mutation analysis : < 1 second
--------------------------------------------------------------------------------
> Total  : 1 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Line Coverage: 5/5 (100%)
>> Generated 8 mutations Killed 7 (88%)
>> Mutations with no coverage 0. Test strength 88%
>> Ran 9 tests (1.12 tests per mutation)
Enhanced functionality available at https://www.arcmutate.com/
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.305 s
[INFO] Finished at: 2022-02-06T16:42:09+01:00
[INFO] ------------------------------------------------------------------------

Y este seria en informe de cobertura en formato html

Informe de cobertura en html

Si vamos al detalle, al contrario que ocurría en java, aquí no se nos muestra el código, pero aun asi nos da pistas de que puede estar mal, en este caso el tercer test ha reemplazado la multiplication por la division y no ha muerto el mutante.

Detalle del informe de cobertura en html

El test es malo, porque hemos elegido multiplicar 1 * 1, y no nos permite detectar errores (1 / 1 = 1, o simplemente return firstNumber darían como bueno ese test). Lo podemos corregir así:

package org.example

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class CalculatorTest {
    private val sut = Calculator()

    @Test
    fun addition_should_add_two_numbers() {
        assertEquals(4, sut.addition(2, 2))
    }

    @Test
    fun subtraction_should_subtract_second_number_from_first_number() {
        assertEquals(6, sut.subtraction(8, 2))
    }

    @Test
    fun multiplication_should_multiply_two_numbers() {
        assertEquals(32, sut.multiplication(8, 4))
    }

    @Test
    fun division_should_divide_first_number_with_the_second() {
        assertEquals(5, sut.division(40, 8))
    }
}

Si volvemos a ejecutar:

mvn test && mvn org.pitest:pitest-maven:mutationCoverage

Tenemos:

================================================================================
- Timings
================================================================================
> pre-scan for mutations : < 1 second
> scan classpath : < 1 second
> coverage and dependency analysis : < 1 second
> build mutation tests : < 1 second
> run mutation analysis : < 1 second
--------------------------------------------------------------------------------
> Total  : 1 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Line Coverage: 5/5 (100%)
>> Generated 8 mutations Killed 8 (100%)
>> Mutations with no coverage 0. Test strength 100%
>> Ran 8 tests (1 tests per mutation)
Enhanced functionality available at https://www.arcmutate.com/
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.410 s
[INFO] Finished at: 2022-02-09T22:29:23+01:00
[INFO] ------------------------------------------------------------------------

Informe de cobertura arreglado

Vemos que ya no hay fallos:

Tests arreglados

5. Conclusiones

Tal y como se puede leer en los artículos de las referencias, mutation testing es un campo de estudio y puede resultar muy útil. En el caso de usar kotlin es sencillo incluirlo en nuestro proyecto, pero es algo menos informativo que cuando lo usamos con java.

Al principio se uso un estilo mas conciso para la definición de la clase como se puede ver a continuación:

package org.example

class Calculator {
    fun addition(firstNumber: Int, secondNumber: Int) = firstNumber + secondNumber

    fun subtraction(firstNumber: Int, secondNumber: Int) = firstNumber - secondNumber

    fun multiplication(firstNumber: Int, secondNumber: Int) = firstNumber * secondNumber

    fun division(firstNumber: Int, secondNumber: Int) = firstNumber / secondNumber
}

Se cambio al que esta en el apartado anterior por si este era el motivo de que no apareciera el código en el informe final, pero no era el caso. Aun así se dejó el que esta actualmente por legibilidad.

Existe un plugin para kotlin en github pero no se encuentra en maven central ni parece que tenga mucho movimiento, pero habrá que estar atentos a ver las herramientas que van saliendo.

6. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad