Pruebas Unitarias: Proveedores de datos


A menudo, cuando realizamos pruebas unitarias de algún problema algorítmico, nos encontramos con muchos tests iguales. Tan solo cambia la entrada y el resultado.

La mayor parte de los XUnit ya se han dado cuenta de esto y por eso han implementado soluciones. El problema es que no se han puesto de acuerdo en la nomenclatura ni en la implementación.

Veremos aquí aproximaciones en Java, PHP y Python.

Java: Parametrized

Poca gente sabe (y yo lo he descubierto hace poco) que JUnit 4 incluyó los tests parametrizados.

En Java todo son clases. No se puede pasar un puntero a función, lo que complica terriblemente cosas realmente sencillas. y éste es uno de esos casos.

Vamos a hacer un ejemplo básico: Fibonacci. Como sabréis, los dos primeros números valen 1 y el resto se genera sumando los dos anteriores. ¡A por ello!

// file src/test/java/fibonacciTest.java
import java.util.Arrays;
import java.util.Collection;
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class FibonacciTest {
    private int input;
    private int expected;
    private Fibonacci sut;

    public FibonacciTest(int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Parameterized.Parameters
    public static Collection numbers() {
        return Arrays.asList(new Object[][]{
                {1, 1},
                {3, 2},
                {4, 3},
                {5, 5},
        });
    }

    @Before
    public void setUp() {
        this.sut = new Fibonacci();
    }

    @Test
    public void testFibonacci() {
        assertEquals(expected, sut.fibonacci(input));
    }
}

Mucho código y poca chicha. Vamos por las cosas importantes:

Cambiando el ejecutor

Pues eso, que tenemos que decirle a JUnit que el ejecutor es Parameterized en lugar del que utiliza por defecto:

@RunWith(Parameterized.class)

Constructor

El constructor debe recibir como argumentos lo que queramos usar en los tests. Es una restricción por utilizar el @Test, que requiere una función sin argumentos.

Generador

Ahora necesitamos una función que genere los argumentos. Ésta será una función estática que devuelve un Collection y marcada con la anotación adecuada:

@Parameterized.Parameters
public static Collection numbers() {
    return Arrays.asList(new Object[][]{
            {1, 1},
            {3, 2},
            {4, 3},
            {5, 5},
    });
}

En este caso he decidido devolver los números a mano, claro.

Tests

Finalmente, los tests, en los que se utilizan los argumentos.

Ejecución

Y ejecutamos los tests normalmente, pero veremos que ha realizado más tests de los habituales (para ejecutarlos usé maven, pero sabéis que no es necesario):

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running FibonacciTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.253 sec

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

El código

Os pego el código de la función principal por si queréis probarlo:

// file src/main/java/fibonacci.java
public class Fibonacci {
    public int fibonacci(int n) {
        int a = 1;
        int b = 1;
        int c;

        for (; n > 2; --n) {
            c = a;
            a = b;
            b = b + c;
        }
        return b;
    }
}

PHP

En PHP todo es más fácil, menos hacer las cosas bien. Aquí lo pondré todo junto:

<?php

function fibonacci($input) {
    $a = 1;
    $b = 1;

    for(; $input > 2; --$input) {
        $c = $a;
        $a = $b;
        $b = $b + $c;
    }
    return $b;
}

class FibonacciTest extends PHPUnit_Framework_TestCase {
    /**
     * @dataProvider numbers
     */
    public function testFibonacci($input, $expected) {
        $this->assertEquals($expected, fibonacci($input));
    }

    public function numbers () {
        return array(
            array(1,1),
            array(3,2),
            array(4,3),
            array(5,5),
        );
    }
}

Como he dicho, todo es más fácil, se entiende más fácil, pero no está bien hecho. Odio tener código en comentarios, y aquí se indica que se utilice el "dataProvider" en un comentario. Los comentarios son para comentar. Y punto.

Pero dejemos ya de juzgar a PHP, cosa que resulta tremendamente sencilla. Vamos a ver lo que he hecho.

Proporcionando datos

Pues nada: una función que devuelve un array de arrays.

Escribiendo el test

Se pone un comentario (eso sí: ¡¡con doble asterisco!!) y se indica qué función es la que va a proporcionar los datos. Y ya.

Cada dato provocará una llamada a la función de tests.

Comparativa entre JUnit y PHPUnit

Como véis, en PHP ha resultado mucho más sencillo. Además, en Java tiene un problema: todos los tests se ejecutarán con todos los datos. No hay manera de especificar distintos proveedores de datos para dos funciones, sino separándolos a dos clases.

En PHP es horrible eso de usar anotaciones embebidas en comentarios. Bueno... Todo en PHP es horrible.

Python

Pues.... Ya he terminado. Python no soporta este tipo de características.

Tienen abierta una incidencia, pero no parecen por la labor de implementarlo. Dicen que es tan sencillo simularlo que no interesa. Yo, personalmente, no estoy de acuerdo. A mí me gusta eso de ver un punto por cada test y que me indique exactamente cuál es el que ha fallado.

Por otra parte, creo que nose proporciona una manera, pero no en la versión disponible en Debian. Quizá cuando sea oficial cambiaré esta entrada :D

De todas maneras, siempre podéis utilizar BDD , mediante freshen , que sí soporta estas características, como ya expliqué en el artículo "Python: Cómo hacer pruebas 5: freshen":link://slug/python-pruebas-5.

Más información

Para Java, os recomiendo la web de Isagoksu, Using JUnit Parameterized Annotation .

Para PHP, el propio manual de PHPUnit, en la sección de Data Providers

Y para Python, ya os dije que no hay nada, y que lo mejor que podéis hacer es usar BDD mediante freshen y mi artículo en castellano "Python: Cómo hacer pruebas 5: freshen":link://slug/python-pruebas-5.


Comentarios

Comments powered by Disqus