style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Aquí va un ejemplo de TDD

Aquí va un ejemplo de TDD.

Hace tiempo que quería escribir esto y con el fin del horario de verano resulta que tengo una hora extra así que aquí va.

No voy a extenderme mucho en que es el TDD ( desarrollo dirigido por pruebas ) ni por que es importante, ni como usar un framework como JUnit o TestNG, espero que baste con decir que el efecto más importante que tiene el TDD es que una vez terminado el desarrollo, el código ya tiene pruebas automatizadas.

Me parece que un ejemplo suficientemente sencillo y no tan trivial puede ser construir un evaluador de expresiones booleanas, donde se pase un string como:

( verdadero y falso ) o verdadero

y nos diga que el resultado es

verdadero

Sé que un evaluador como este puede ser escrito muy fácilmente con un generador lexico, pero no es la idea ahora. La idea es hacer un ejemplo que sea suficientemente interesante.

Antes de empezar repasemos cual es el ciclo de vida del TDD:

  • Agregar una prueba ¡¡ANTES!! ( esto es importante )
  • Correr las pruebas y ver si el la nueva prueba falla
  • Escribir código *mínimo y suficiente*para hacer que la prueba pase
  • Correr las pruebas y ver si no hemos roto alguna prueba existente
  • Hacer refactoring
  • Repetir hasta quedar satisfechos

Entonces empecemos,

1.- Agregamos la primera prueba:

package tdd.demo;

import org.testng.annotations.Test;

/**
 * User: oscarryz
 * Date: Nov 1, 2010
 */

@Test
public class EvaluadorTest {
    public void testTrue() {
        EvaluadorBoleano evaluador = new EvaluadorBoleano();
        assert evaluador.evalua("verdadero") == true;
    }
}

2.- Vemos que falle.

Un momento, creo que eso fue demasiado demasiado breve. Ni siquiera compila, por que aún no existe la clase EvaluadorBoleano pero aquí hay algo importante; al crear la prueba, voy a describir como quiero que se llame mi clase, como es que va a ser usada y que métodos va a tener, que parámetros y todo ¡mientras la pruebo!. Es algo extremo ya sé, no en vano el TDD forma parte de la metodología de programación extrema ( XP ). Si cambiamos de opinión en el futuro ( más bien, si las pruebas nos hacen cambiar de opinión ) podemos sin mucho temor, cambiar alguna firma o método y luego volver a correr las pruebas y podemos ver que es lo que se rompe. Aún así, por el momento podemos decir que ejecutamos bien el paso 2.

3.- Escribir el código mínimo para que pase.

En este punto es demasiado sencillo. Como soy muy positivo, nomás devuelvo "true"

package tdd.demo;

/**
 * User: oscarryz
 * Date: Nov 1, 2010
 */

public class EvaluadorBoleano {
    public boolean eval(String s) {
        return true;
    }
}

4.- Correr las pruebas existente.

Corremos las pruebas y vemos que todo este en verde:

en verde

Parecerá ridículo que si mi método regresa true "hardcodeado" yo diga que ya funciona, pero la verdad es que la funciona para el conjunto de pruebas que tengo. Es como si dijera, "así es como la pretendo usar". Obvio, no cumple con todos mis expectativas, pero la idea es que no codifique nada más de lo que estoy probando, es decir, hay que evitar hacer suposiciones, evitar adelantar código, evitar pensar *"Y si quiero hacer esto? Y si que va a pasar con esto otro? etc". Si se nos ocurren más escenarios para nuestro código, lo que debemos hacer es: escribir una prueba PRIMERO y el código mínimo para pasar esa prueba DESPUÉS ( ¿por eso se llama dirigido por pruebas no? )

5.- Hacer refactoring.

A veces este punto no aplica, como en este caso así que seguimos.

6.- Repetir.

Así que vamos a la segunda prueba ¿Cual podría ser?, Pos claaaro!!!, probar con "falso" no?

  • Agregar prueba

    public void testFalse() {
        EvaluadorBoleano evaluador = new EvaluadorBoleano();
        assert evaluador.eval("falso") == false;
    }

  • Ver si falla

Esta vez espero que falle, por que no he hecho nada para que funcione.

alt text

Estatus 2 pruebas fallando 1

  • Escribir el código para que pase

Aquí viene toda la magia ( que no es mucha ) ¿Qué tengo que escribir para que mi prueba pase? ¿Que tan complejo debe de ser mi código? Respuesta, LO MÁS SIMPLE, POSIBLE, a veces incluso hay que exagerar.

Entonces el código que hace que mi prueba pase es:

package tdd.demo;

/**
 * User: oscarryz
 * Date: Nov 1, 2010
 */

public class EvaluadorBoleano {
    public boolean eval(String expresion) {
        if( "verdadero".equals(expresion)) {
            return true;
        } else {
            return false;
        }
    }
}

  • Correr las pruebas

Vemos si es cierto corriendo las pruebas:

verde de nuevo

Verde de nuevo.

Parece ridículo a esta altura pero estas son las bases de TDD.

  • Repetir

Entonces. Tenemos que el ciclo es este:

test -> falla -> fix -> verde -> refactoring -> repetir

Vamos a poner más código.

Mi siguiente prueba, será parentesis:

public void testParentesis() {
    EvaluadorBoleano evaluador = new EvaluadorBoleano();
    assert evaluador.eval("(verdadero)") == true;        
    assert evaluador.eval("(false)") == false;
}

Rojo. ¿Lo mínimo para componerlo? Quitarle los paréntesis. doh!..

package tdd.demo;

/**
 * User: oscarryz
 * Date: Nov 1, 2010
 */

public class EvaluadorBoleano {
    public boolean eval(String expresion) {
        if( expresion.startsWith("(") &&  expresion.endsWith(")")){
            expresion = expresion.substring(1, expresion.length()-1);
        }
        return "verdadero".equals(expresion);
    }
}

Se me ocurre validar también los espacios en blanco. Agrego otro test:

public void testEspacios() {
    EvaluadorBoleano evaluador = new EvaluadorBoleano();
    assert evaluador.eval(" verdadero  ");
    assert !evaluador.eval(" falso    ");
    assert evaluador.eval( " ( verdadero   )     ");
    assert !evaluador.eval( " ( falso   )     ");
}

Rojo, de nuevo. ¿Lo mínimo para que funcione? Ignorar los espacios

public boolean eval(String expresion) {
    expresion = expresion.trim();
    if( expresion.startsWith("(") &&  expresion.endsWith(")")){
        expresion = expresion.substring(1, expresion.length()-1).trim();

    }
    return "verdadero".equals(expresion);
}

Verde de nuevo.

Por que no probamos con el operador "y"

public void testY() {
    assert evaluador.eval("verdadero y verdadero");
    assert !evaluador.eval("verdadero y falso");
}

Rojo.

¿Que será lo mínimo esta vez? Se me ocurre, que si tiene una "y" , entonces partir por la letra "y" y ver si las partes dan verdadero.

Funcionó:

public boolean eval(String expresion) {
    expresion = expresion.trim();
    if( expresion.startsWith("(") && expresion.endsWith(")")){
        expresion = expresion.substring(1, expresion.length()-1).trim();

    }
    String[] partes = expresion.split("y");
    if( partes.length > 1 ) {
        boolean parcial = true;// todos son verdaderos hasta que se demuestre lo contrario.
        for( String parte : partes ) {
            parcial = parcial && eval( parte  );
        }
        return parcial;
    }
    return "verdadero".equals(expresion);
}

Verde de nuevo

Acá hay algo importante.

El TDD no es necesariamente algo fácil. Es muy útil, pero se requiere experiencia previa en programación, para ver algunas cosas "a futuro".

Es muy fácil, hacer las pruebas incorrectas, que nos darán la falsa impresión de que estamos avanzando, cuando pudiera ser que estemos dando vueltas en círculo. Lo bueno, es que este tipo de cosas se aprenden con la práctica y al fin del día, las pruebas hablan por si mismas.

Lo peor que nos puede pasar es que tardemos un poco.

En este ejemplo yo hice una funcion recursiva para evaluar las partes. Claro, tengo que saber que es una funcion recursiva y cuando aplicarla etc.

Seguimos. ¿Cual podría ser mi siguiente prueba? Podría ser probar usando paréntesis. Según yo esto debería de jalar bien:

public void testYConParentesis() {
    assert evaluador.eval("(verdadero ) y verdadero");
    assert !evaluador.eval("( falso ) y verdadero");
    assert evaluador.eval("(verdadero ) y ( verdadero )");
    assert !evaluador.eval("(verdadero ) y ( falso )");
}

Oops, me dio rojo.. mmmhh a veces no es tán fácil saber que está pasando. Sobre todo porque el mensaje de eror es un stacktrace como este:

java.lang.AssertionError
 at tdd.demo.EvaluadorTest.testYConParentesis(EvaluadorTest.java:43)

No dice mucho más de lo que ya sé.

Lo que yo hago ( y quizá sea una mala práctica ) es poner logs para entender que es lo que sucede ( aunque desafía el propósito, pues el TDD sirve precisamente para deshacerse de los logs para probar ) Cuando entiendo que pasa se los quito.

En este caso mi prueba dice: "verdadero )" y eso hace que falle. Revisando mi código dice

    if( expresion.startsWith("(") && expresion.endsWith(")")){

Es decir, si empieza y termina con "(" y con ")", entonces quitaselo, pero no dice que hacer si solo empieza o solo termina con paréntesis. Una vez detectado el problema, lo podemos arreglar y continuar:

Verde de nuevo:

verde de nuevo

Y quitamos el log.

Ya podemos seguir con el "o" ( or lógico )

public void testO(){
    assert evaluador.eval("verdadero o verdadero");
    assert evaluador.eval("falso o verdadero");
}

Lo arreglamos... mhh creo que no es tán sencillo por que yo tengo estoy separando por cada "y" que me encuentro. Por que no mejor, evaluo la primera parte y dejo que el resto se evalue recursivamente:

public boolean eval(String expresion) {
    err.println("expresion = " + expresion);
    expresion = expresion.trim();
    if( expresion.startsWith("(")) {
        expresion = expresion.substring(1).trim();
    }
    if( expresion.endsWith((")"))) {
        expresion = expresion.substring(0, expresion.length()-1).trim();
    }

    int indexOfY = expresion.indexOf("y");
    if( indexOfY > 0 ) {
        return eval( expresion.substring(0,indexOfY) ) && eval( expresion.substring(indexOfY+1));

    }

    return "verdadero".equals(expresion);
}

Ok, creo que está mejor, ahora ver si sirve para el or:

public boolean evalua(String expresion) {
    err.println("expresion = " + expresion);
    expresion = expresion.trim();
    if( expresion.startsWith("(")) {
        expresion = expresion.substring(1).trim();
    }
    if( expresion.endsWith((")"))) {
        expresion = expresion.substring(0, expresion.length()-1).trim();
    }

    int indexOfY = expresion.lastIndexOf(" y ");
    if( indexOfY > 0 ) {
        return evalua( expresion.substring(0,indexOfY) ) && evalua( expresion.substring(indexOfY+3));

    }
    int indexOfO = expresion.indexOf(" o ");
    if( indexOfO > 0 ) {
        return evalua( expresion.substring(0,indexOfO) ) || evalua( expresion.substring(indexOfO+3));

    }

    return "verdadero".equals(expresion);
}

Funcionó. Aún le hace falta mucho más código y pruebas. Por ejemplo, que evalué correctamente la precedencia de los paréntesis, o que pueda combinar varias expresiones anidadas o que soporte otro operador como el xor. En fin, este pretendía ser un ejemplo breve de como hacer el TDD, aunque por lo visto no fue tan breve.

Lo realmente interesante es el efecto que tiene esta practica en la calidad del software a la larga, sobre todo en proyectos grandes. Vi hace poco un dashboard de Google Chrome donde venía un estatus de todos los builds automatizados que corren diariamente. Todo esto es posible gracias a que gran parte del código de Google Chrome se hace con TDD.

Ahi va el link:

http://build.chromium.org/p/chromium/waterfall

Espero que esto les resulte útil.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de ezamudio

Muy bueno

Buena guía práctica a TDD.

La manera como yo lo veo es que te pones en plan "usuario" sin dejar de ser programador. Como cuando eres usuario de software para desarrollar, por ejemplo cuando eres usuario de Spring o algo así. Pero en este caso te estás poniendo como usuario de tu propio software; eso de que antes de escribir tu clase primero hagas la prueba y ahí le pones el nombre y los métodos y los parámetros, te hace pensar en cómo se debe USAR esa clase y entonces la vas a diseñar de modo que sea fácil de usar, no que sea fácil de implementar y que usarla sea un rollo. Eso es muy útil sobre todo en desarrollo de APIs y de bibliotecas de software.

Si, muy útil de verdad

La explicación fue clara y breve. Es un buen ejemplo de cómo programar correctamente (a mi parecer). Los profesores de programación deberian checar este post para que entiendan un poquito de "lo que enseñan".

Buen post.

Imagen de rodrigo salado anaya

Practicando TDD...

Tiene poco que hice mis primeros pininos con esto de TDD ( y me gusto mucho solo que me falta refinar muchas cosas...), y me paso algo muy parecido a ti, al agregas los ( ) , y pues recuerdo que se tiene que hacer lo mínimo como prueba. En este caso según YO y se que es un ejemplo, pero debiste meter solo un paréntesis por ves y luego los dos, claro fallaría con un error tal ves diferente.

Y platicando de otro tema y como siempre por falta de mucho tiempo libre ¿por qué no habrá wi-fi en el suburbano? jejejeje, me clave con hacer algo con System.console y pues no he investigado como hacer pruebas unitarias desde consola, pero bueno. Lo que me gusto mucho de TDD es que gastas mucho tiempo en hacer pruebas pero tardas poco en encontrar tus errores.

Gracias por el aporte esta genial.
: D

Imagen de Sr. Negativo

TDD: desarrollo guiado por pruebas

Al igual que @rodrigo estuve practicando algo de TDD. Es dificil iniciar con las pruebas antes de codificar el programa (nunca habia hecho esto).

Lo que me confunde es que algunos autores manejan entre 3, 4 o hasta 7 pasos para realizar las pruebas.

Pero me quedo con estos:

  1. Escribir el test (rojo --> prueba fallida)
  2. Implementar código según test (verde --> prueba que pase)
  3. Refactorizar para eliminar duplicidad, código muerto y hacer mejoras

p.s Si, ya sé post pasado pero sigue sirviendo.

TDD para desarrollar RyzC ( El compilador de Ryz )

Así es:

test -> rojo
fix   -> verde
refactoring y repetir

Solo recuerda. Cuando se hace refactoring se hacen mejoras en el diseño, pero en lo que hace!. No se debe de agregar ni quitar ninguna funcionalidad o bug.

El refactoring es lo más importante del TDD y generalmente lo que siempre se omite.

Yo por ejemplo he trabajado en un pequeño compilador con puro TDD ( en realidad es más como BDD pero que más da ) donde en vez de utilizar un método tradicional de escribir un Lexer -> Parser -> IR -> Código me fuí directamente a escribir mi primera prueba así:

    /** Prueba que se pueda cargar un .class dinámicamente */
   public void loadClass() throws MalformedURLException, ClassNotFoundException {
        // Ver que no esté la clase:
        try {
            ClassLoader.getSystemClassLoader().loadClass("hello.world.Hello");
            throw new AssertionError("Error. Debió tronar al intentar cargar la clase");
         } catch (ClassNotFoundException e) {
            // Si llego aquí está bien porque tiró ClassNotFoundException
         }
         // Cargarla dinámicamente:
        URL[] urls = new URL[]{new URL(new File("").toURI()+"resources/")};
        new URLClassLoader(urls).loadClass("hello.world.Hello");

    }

Y claro , como no había hecho absolutamente nada, pues me salía rojo porque jamás se cargaba la clase.

Luego para hacer pasar el test, lo que hice simple y sencillamente fue... pegar un archivo .class generado con un compilador existente. Eso fue lo más simple que se me ocurrió. Lo hice y salió en verde. Si alguien quiere ver esa primera prueba puede ver esto

Mi segunda prueba fue, probar que la clase no existía, compilarla y luego probar que ya existe:

 
public void compileAndLoad() throws IOException, ClassNotFoundException {
    String className = "load.test.First";
    assertMissing(className);
    ryzc.compile("First.ryz");
    assertExists(className);
}

Y para hacerlo pasar que es lo más sencillo? Pues utilizar el compilador de Java y compilar una clase fija:

        FileWriter writer     = new FileWriter(sourceFile);
        writer.write(
            "package load.test;\n" +
            "public class First{}"
        );
        writer.close();
        // usar javac y compilar este achivo

Y así he seguido, luego probar compilar un archivo que no existe, luego cambiar el nombre del archivo, cambiar el nombre de la clase, etc. etc. hasta probar que se crea un bloque de código ( akka Closure ) y probar métodos de extensión entre otras cosas.

Ahora... lo que he hecho mal ( muy mal de hecho ) es que, de acuerdo al TDD, los patrones emergerán de estas pruebas. Cuando los patrones emergan se tienen que hacer un refactoring para adaptar el diseño. Lo que yo he hecho mal ( como decía ) es muchas veces saltarme este refactoring y enterrar estos patrones bajo plastas de copy/paste ( si, ya lo dije, y por eso sé de cierto que el copy/paste es la causa de todos los males )

Pero si se tiene una buena disciplina se pueden aprovechar estos patrones y mejorar el diseño del código y permitir futuras modificaciones más fácilmente.

Ahora que saben que el RyzC usa TDD , alguien se anima? Lo único que se necesita es hacer un test que falle para agregar alguna funcionalidad ( structural typing por ejemplo ) y luego hacer el código mínimo para que pase :)

Saludos

Imagen de beto.bateria

interesante

interesante

Imagen de Sr. Negativo

TDD ... algo dificil

@OscarRyz

El TDD es dificil, cambiar la forma en que diseñas y escribes el código no es nada sencillo. Sin embargo, es necesario probar el buen funcionamiento del código antes de pasarlo a producción.

Ahorra muchos problemas a la hora de implementar el código. Además obliga al programador/diseñador/analista a prepararese mejor.

p.s A ver me voy a animar a probar Ryz :)

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">