Usar DataProvider de TestNG

Premisas
Antes de empezar con este post establezcamos unas premisass que serán ciertas para entendernos mejor:

  • Probar el código es bueno.
  • Probarlo automáticamente es mejor
  • Usar un framework para pruebas facilita las cosas.

Habiendo establecido que estas que agregar pruebas es una buena práctica ( aunque no debemos de sentirnos mal, si lo hacemos ), podemos continuar.

Escenario
Cuando se están agregando pruebas y más pruebas y más y más pruebas en el código ( por ejemplo cuando se usa TDD ) sucede muchas veces que lo único que cambia en las nuevas pruebas es la entrada y la salida, pero no necesariamente la forma de validarlo, es decir, no se están agregando pruebas nuevas para nueva funcionalidad, sino que se están agregando más y más casos para una funcionalidad existente.

Cuando esto sucede, resulta molesto tener que escribir un nuevo test que haga lo mismo que hace un test existente donde solo cambien los parametros de entrada y la salida esperada. En el peor de los casos se hace copy/paste del código, pero a algunos eso los hace sentir sucios ( a mí por ejemplo, pero generalmente ma aguanto ). Un programador sensato crearía un método de utilería y simplemente que se encargara de ejecutar la comprobación.

Ejemplo, antes:

public void testA()  {
    String intput   = "A";
    String expected = "Alpha";
    thing.doX( input );
    thing.doY();
    thing.doZ();
    String actual = thing.getOutput();
    assert expected.equals( actual );
}

public void testB()  {
    String intput = "B";
    String expected = "Beta";
    thing.doX( input );
    thing.doY();
    thing.doZ();
    String actual = thing.getOutput();
    assert expected.equals( actual );
}

public void testD()  {
    String intput = "D";
    String expected = "Delta";
    thing.doX( input );
    thing.doY();
    thing.doZ();
    String actual = thing.getOutput();
    assert expected.equals( actual );
}

Después:

public void testA()  {
    testUtil( "A", "Alpha" );
}

public void testB()  {
    testUtil( "B", "Beta" );
}

public void testD()  {
   testUtil( "D", "Delta" );
}
private void testUtil( String input, String expected ) {
    thing.doX( input );
    thing.doY();
    thing.doZ();
    String actual = thing.getOutput();
    assert expected.equals( actual );
}

Lo cual está bien por cierto tiempo, pero cuando pasa los días las semanas y los meses ( nahh bueno no tanto ) ese patroncito de agregar:

public void test testP() {
    testUtil( "P", "Rho" );
}
public void testT() {
    testUtil("T", "Tau" );
}

Se vuelve de nuevo bastante molesto y a algunos los hace sentir sucios ( ¬¬ Ok, ok a mí me incomoda ).

¿Que se puede hacer?

Alternativa

Un test framework nos debe de ayudar en este tipo de cosas. El que yo conozco y uso es TestNG ( Test New Generation, seee, los desarrolladores no siempre somos buenos para los nombres ) creado por Cedric Beust tiene una funcionalidad que ayuda en este propósito. Desconozco si JUnit lo tiene ( seguramente sí, pues JUnit v4 fue básicamente un remake de TestNG según Cedric, noooo me consta ), pero es muy útil, sobre todo si el código para obtener los datos de entrada y de salida involucran algún recurso externo como un filesystem (por ejeeeeeeplo).

Esta funcionalidad se llama DataProvider es una "anotación" y funciona así:

1.- Un test dice quién es su proveedor de datos
2.- Un método X es anotado como proveedor de datos
3.- Tan tan..

Ejemplo:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

@Test
public class TestTestProvider {
    @Test( dataProvider = "getAllData" )
    public void testLetter( String input, String expectedOutput ) {
        /*
           Do some magic with input...
           generate the output...
         */

        String output = "Beta";  // <-- generated dynamically
        assert output.equals(expectedOutput);

    }
    @DataProvider(name = "getAllData")
    private Object [][] loadSourceFiles() {
        // read the filesystem
        // load the input and output
        // put them in an array:
        return new Object[][]{
                {"A","Alpha"},
                {"B","Beta"},
                {"D","Delta"}
        };

    }
}

Nota ojo que en mi código el cuerpo del test es totalmente irrelevante, lo interesante aquí es ver como funcionan las anotaciones DataProvider de TestNG

Da da!!!, el framework se puso a trabajar para nosotros como debe de ser.

Como funciona es que el método marcado como DataProvider va a regresar una matriz de objetos, donde cada "renglón" ( por llamarlo de alguna forma ) será una invocación a un método o será mejor dicho un test por sí solo. Y cada "columna" será un parametro que se pasará del test. Así por ejemplo en mi método el primer elemento es la entrada y el segundo la salida esperada, pero como finalmente son datos de tipo Object pueden ser cualquier cosa ( el primero ser la salida y el segundo la entrada, o tener N entradas o una sola etc. etc. )

Entonces en el código anterior se invocarían tres pruebas:

{"A","Alpha"},  <-- Una
{"B","Beta"},   <-- Dos
{"D","Delta"}   <-- Tres

Una por renglón y cada columna sería un parametro de entrada, sería el equivalente a llamar:

...
testLetter( "A", "Alpha" );
testLetter( "B", "Beta" );
testLetter( "D", "Delta" );
...

Con esto se ahorra mucho tiempo en andar haciendo cosas que son son propiamente el test, sino de la infraestructura del test y como todo mundo dice: ... te permite enfocarte en tu "negocio" :) . En el caso de TestNG esto es cierto.

Yo por ejemplo, tengo al momento unos 60 casos de prueba donde la entrada son archivos y la forma de cargarlos y probarlos es exactamente la misma para todos.

La única desventaja que tiene esto, es que si uno de ellos falla, al reintentar los fallidos corre todos de nuevo ( cuando lo natural es que corriera solo los fallidos, o quizá no entendí yo bien algo ) pero bueno, en mi caso es una desventaja menor y quizá ya haya un ticket con eso.

Espero que esto les sea útil.

Update
Nuestro buen amigo Abaddon, nos dice que efectivamente está en JUnit 4
http://www.mkyong.com/unittest/junit-4-tutorial-6-parameterized-test/

El uso es casi el mismo, buen finding.