Tutorial

Esta guía explica los mecanismos básicos para pasar de especificaciones a especificaciones activas usando Concordion. No debería tomarle más de 15 a 30 minutos el completarla, asumiendo que ya se está familiarizado con Java, JUnit y HTML.

Instalación

Concordion requiere JDK 5.0 o superior y los siguientes JARs en el classpath:

Nota: Todos los JARs están incluidos en la distribución.

Una especificación activa Concordion consiste en dos partes: (i) un documento HTML bien formado describiendo la funcionalidad, y (ii) código de fijación (fixture) escrito en Java (una extensión especial de Concordion the un caso de prueba JUnit) que descubre ejemplos concretos en el documento y los usa para verificar el sistema en pruebas (SUT). Ambos ficheros deben estar en el mismo paquete (package).

Para que ocurra la magia, el documento debe ser antes instrumentado con comandos.

Los comandos Concordion son especificados como atributos de elementos en el documento HTML. Los navegadores web ignoran los atributos que no entienden, de modo que estos comandos son invisibles a efectos prácticos.

Los comandos usan el espacio de nombres "concordion" definido al principio de cada documento como sigue:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">

Empecemos con un ejemplo realmente sencillo...

concordion:assertEquals

  1. Creemos un paquete Java llamado "example".
  2. Creemos un fichero "HelloWorld.html" dentro del paquete conteniendo:
    <html>
        <body>
            <p>Hello World!</p>
        </body>
    </html>
    
  3. Ahora instrumentemos el fichero como sigue:
    <html xmlns:concordion="http://www.concordion.org/2007/concordion">
        <body>
            <p concordion:assertEquals="getGreeting()">Hello World!</p>
        </body>
    </html>
    
  4. En el mismo paquete example, creemos un fichero Java "HelloWorldTest.java" conteniendo:
    package example;
    
    import org.concordion.integration.junit3.ConcordionTestCase;
    
    public class HelloWorldTest extends ConcordionTestCase {
    
        public String getGreeting() {
            return "Hello World!";
        }
    }
    
  5. Ahora ejecutemos la clase HelloWorldTest usando JUnit.

Si lo ha hecho correctamente, JUnit debería mostrarle una barra en verde y un mensaje como el que sigue debería ser mostrado en la consola:

C:\temp\concordion-output\example\HelloWorld.html
Successes: 1  Failures: 0

El mensaje muestra la trayectoria del fichero de salida (resultado) para la prueba y un resumen con los contadores de éxitos y fracasos en las pruebas. Por defecto, Concordion produce la salida en el directorio especificado en la propiedad del sistema java.io.tmpdir.

Abra el fichero de salida en un navegador y podrá ver el mismo contenido que en el fichero de entrada, pero con las palabras Hello World! resaltadas en verde.

Hello World! successful outcome

Propiedades Java Bean

En el ejemplo de arriba, la llamada a "getGreeting()" puede simplificarse a "greeting" ya que el lenguaje de expresiones de Concordion puede manejar propiedades sencillas del tipo Java Bean.

<html xmlns:concordion="http://www.concordion.org/2007/concordion">
    <body>
        <p concordion:assertEquals="greeting">Hello World!</p>
    </body>
</html>

concordion:set

Dada una especificación como ésta:

<html>
    <body>
        <p>
            El saludo para el usuario Pepe será: ¡Hola Pepe!
        </p>
    </body>
</html>

Queremos que el nombre ("Pepe") sea un parámetro y que el saludo ("¡Hola Pepe!") sea verificado contra el resultado devuelto por el sistema.

Para hacer esto ponemos etiquetas <span> alrededor de los dos textos relevantes dentro del documento. En HTML, las etiquetas <span> no tienen ningún efecto al mostrar la salida del documento.

<html>
    <body>
        <p>
            El usuario para el usuario <span>Pepe</span>
            será: <span>¡Hola Pepe!</span>
        </p>
    </body>
</html>

Ahora podemos instrumentar el documento:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">
    <body>
        <p>
            El saludo para el usuario <span concordion:set="#firstName">Pepe</span>
            será:
            <span concordion:assertEquals="greetingFor(#firstName)">¡Hola Pepe!</span>
        </p>
    </body>
</html>

Cuando Concordion procesa el documento, temporalmente establecerá el valor de la variable #firstName a "Pepe" y entonces llamará al método greetingFor() con ese valor y comprobará que el resultado es igual que "¡Hola Pepe!".

Nuestro código Java de la fixture necesitará ser modificado:

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class HelloWorldTest extends ConcordionTestCase {

    public String greetingFor(String firstName) {
        return "BLA";
    }
}

Justo igual que cuando escribimos pruebas unitarias, siempre hacemos fallar la prueba antes de hacer que pase, para darnos confianza en que estamos realmente probando algo. Con el código tal y como está ahora deberíamos obtener un fallo (pues esperamos "¡Hola Pepe!" pero obtenemos "BLA").

Ahora arreglamos el código:

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class HelloWorldTest extends ConcordionTestCase {

    public String greetingFor(String firstName) {
        return "¡Hola " + firstName + "!";
    }
}

Obviamente, en una aplicación real, la implementación de greetingFor() sería bastante diferente. El comportamiento no estaría implementado aquí sino en el código del sistema que estamos probando y la fixture simplemente llamaría al sistema. Esto se podría hacer a nivel del sistema o a un nivel más bajo: se podría incluso llamar directamente como en una prueba unitaria si escribir una prueba de sistema fuera muy lento o difícil de realizar.

concordion:execute

El comando execute tiene tres usos principales:

  1. Ejecutar una instrucción cuyo resultado es "void".
  2. Ejecutar una instrucción cuyo resultado es un objeto (para permitir comprobar varias propiedades de dicho objeto).
  3. Manejar frases con estructuras poco habituales.

Ejecutar una instrucción cuyo resultado es void

En ocasiones puede ser útil ejecutar una instrucción que establezca de alguna manera el estado del sistema. Cada vez que hacemos esto, sin embargo, deberían sonar alarmas en nuestras cabezas y preguntarnos si no estaremos (inadvertidamente) escribiendo un script en vez de una especificación. Por ejemplo, una llamada a "clearDatabase()" sería un evidente mal uso (ver Técnicas (en inglés) para saber más sobre este tema).

Como regla general, los métodos que devuelven void llamados desde un execute deberían comenzar con la palabra set o setUp. P.ej. setUpUser(#username).

Tomemos la siguiente especifación como ejemplo:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">
    <body>
        <p>
            Si la hora es:
            <span concordion:set="#time">09:00AM</span>
            <span concordion:execute="setCurrentTime(#time)" />
            entonces el saludo será:
            <span concordion:assertEquals="getGreeting()">¡Buenos Días Mundo!</span>
        </p>
    </body>
</html>

Nuestro código Java de la fixture será como éste:

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class HelloWorldTest extends ConcordionTestCase {

    public void setCurrentTime(String time) {
        // TODO
    }

    public String getGreeting() {
        return "TODO";
    }
}

Realmente nosotros podemos eliminar la necesidad de usar ese comando concordion:set usando en su lugar la variable especial #TEXT (la cuál contiene el texto del elemento actual). La instrumentación abreviada queda así:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">
    <body>
        <p>
            Si la hora es:
            <span concordion:execute="setCurrentTime(#TEXT)">09:00AM</span>
            entonces el saludo será:
            <span concordion:assertEquals="getGreeting()">¡Buenos Días Mundo!</span>
        </p>
    </body>
</html>

Una alternativa sería cambiar la firma del método getGreeting() para permitir pasarle la hora como un parámetro. Este el camino que normalmente tomaríamos. Un execute sin valor de retorno es una mala señal; p.ej. estamos escribiendo un script o nuestra especificación contiene demasiadas variables y cubre demasiados comportamientos. Sin embargo, la funcionalidad está ahí por si la necesitamos.

Ejecutar una instrucción cuyo resultado es un objeto

A veces necesitamos comprobar más de un resultado para un comportamiento dado. Por ejemplo, aquí queremos comprobar que tanto el nombre como el apellido son extraídos correctamente a partir del nombre completo:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">

    <head>
        <link href="../concordion.css" rel="stylesheet" type="text/css" />
    </head>

    <body>

        <h1>Separando Nombres</h1>

        <p>
            Para ayudar a personalizar nuestros envíos postales, queremos tener
            el nombre y el apellido del cliente. Desgraciadamente los datos del
            cliente que nos han proporcionado sólo contienen nombres completos.
        </p>

        <p>
            Por lo tanto el sistema intenta separar un nombre completo en sus
            componentes separándolos por el espacio en blanco.
        </p>

        <div class="example">

            <h3>Ejemplo</h3>

            <p>
                El nombre completo
                <span concordion:execute="#result = split(#TEXT)">Juan Pérez</span>
                se separará en nombre
                <span concordion:assertEquals="#result.firstName">Juan</span>
                y apellido
                <span concordion:assertEquals="#result.lastName">Pérez</span>.
            </p>

        </div>
    </body>
</html>

La variable #result será un objeto devuelto por el método split(). Este objeto tendrá unas propiedades firstName y lastName.

Asumiendo que nuestro fichero HTML está en el paquete example y que se llama "SplittingNames.html", entonces necesitamos una fixture Java llamada SplittingNamesTest:

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class SplittingNamesTest extends ConcordionTestCase {

}

Si ejecutamos la fixture tal y como está (e.d. vacía), la salida sería así:

La salida muestra un test roto (debido a la ausencia de código de fijación)

Nos dice lo que tenemos que hacer. Arreglamos nuestro código de fijación:

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class SplittingNamesTest extends ConcordionTestCase {

    public Result split(String fullName) {
        return new Result();
    }

    class Result {
        public String firstName = "TODO";
        public String lastName = "TODO";
    }
}

Lo ejecutamos ahora y obtenemos:

La salida muestra un test roto porque la fixture no está completamente implementada

Implementemos la función. Obviamente, la implementación debería estar en el sistema real, no en el caso de prueba, pero sólo estamos jugando...

package example;

import org.concordion.integration.junit3.ConcordionTestCase;

public class SplittingNamesTest extends ConcordionTestCase {

    public Result split(String fullName) {
        Result result = new Result();
        String[] words = fullName.split(" ");
        result.firstName = words[0];
        result.lastName = words[1];
        return result;
    }

    class Result {
        public String firstName;
        public String lastName;
    }
}

Ahora el test pasa:

Ahora el test pasa

Nota: Nuestra inner class Result podría también ser implementada con getters en vez de con campos públicos: la instrumentación del HTML no cambia por ello.

class Result {

    private final String firstName;
    private final String lastName;

    public Result(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Manejar frases con estructuras poco habituales

Una de las grandes cosas de Concordion es que cuando estás escribiendo las especificaciones no te tienes que preocupar acerca de cómo las vas a instrumentar. Simplemente te concentras en hacer el documento lo más legible posible.

La mayoría de las frases se pueden instrumentar. Si no puedes encontrar cómo instrumentarla, siempre puedes jugar con las palabras, pero en general esto no debería ser necesario. El comando execute proporciona esta flexibilidad.

Por ejemplo, digamos que tenemos la especificación:

<p>
    Tras acceder al sistema, el saludo que recibirá el usuario <span>Pepe</span>
    será: <span>¡Hola Pepe!</span>
</p>

Esto es fácil de instrumentar:

<p>
    Tras acceder al sistema, el saludo que recibirá el usuario
    <span concordion:set="#firstName">Pepe</span>
    será:
    <span concordion:assertEquals="greetingFor(#firstName)">¡Hola Pepe!</span>
</p>

Pero que hubiera pasado si nuestra especificación se hubiera escrito como sigue:

<p>
    Se debería mostrar el saludo "<span>¡Hola Pepe!</span>" al usuario
    <span>Pepe</span> cuando éste acceda al sistema.
</p>

En este caso, el parámetro de entrada Pepe aparece después de la salida que queremos comprobar (el saludo). Podemos resolver este problema usando un comando execute en el elemento más exterior (el <p>).

<p concordion:execute="#greeting = greetingFor(#firstName)">
    Se debería mostrar el saludo "<span concordion:assertEquals="#greeting">¡Hola Pepe!</span>"
    al usuario <span concordion:set="#firstName">Pepe</span>
    cuando éste acceda al sistema.
</p>

¿Cómo funciona esto? Esto funciona porque el comando execute está diseñado para procesar comandos en sus elementos hijo en un orden especial. Antes que nada procesa cualquier comando set hijo, entonces ejecuta su propio comando y finalmente procesa cualquier comando assertEquals hijo.

concordion:execute en una <table>

Cuando queremos mostrar varios ejemplos de un comportamiento, repitiendo la misma estructura de una frase una y otra vez, probablemente esto no sea muy fácil de leer. Sería mejor usar una tabla.

Por ejemplo:

Cómo se muestra la tabla (simple y claro)

Podemos instrumentar esta tabla, de una manera un poco larga, como sigue:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link href="../concordion.css" rel="stylesheet" type="text/css" />
    </head>

    <body>

        <h1>Separando Nombres</h1>

        <p>
            Para ayudar a personalizar nuestros envíos postales, queremos tener
            el nombre y el apellido del cliente. Desgraciadamente los datos del
            cliente que nos han proporcionado sólo contienen nombres completos.
        </p>

        <p>
            Por lo tanto el sistema intenta separar un nombre completo en sus
            componentes separándolos por el espacio en blanco.
        </p>

        <div class="example">

            <h3>Ejemplos</h3>

            <table>
                <tr>
                    <th>Nombre Completo</th>
                    <th>Nombre</th>
                    <th>Apellido</th>
                </tr>
                <tr concordion:execute="#result = split(#fullName)">
                    <td concordion:set="#fullName">Juan Pérez</td>
                    <td concordion:assertEquals="#result.firstName">Juan</td>
                    <td concordion:assertEquals="#result.lastName">Pérez</td>
                </tr>
                <tr concordion:execute="#result = split(#fullName)">
                    <td concordion:set="#fullName">Felipe Reyes</td>
                    <td concordion:assertEquals="#result.firstName">Felipe</td>
                    <td concordion:assertEquals="#result.lastName">Reyes</td>
                </tr>
            </table>

        </div>
    </body>
</html>

Sin embargo, esto es repetitivo, por lo que Concordion proporciona una alternativa más cómoda. Cuando ponemos un comando execute en un elemento <table>, los elementos de la cabecera (la fila que contiene los elementos <th>) son copiados a cada linea de detalle (las filas que contienen los elementos <td>) y se ejecuta el comando execute en cada linea de detalle.

<html xmlns:concordion="http://www.concordion.org/2007/concordion">

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link href="../concordion.css" rel="stylesheet" type="text/css" />
    </head>

    <body>

        <h1>Separando Nombres</h1>

        <p>
            Para ayudar a personalizar nuestros envíos postales, queremos tener
            el nombre y el apellido del cliente. Desgraciadamente los datos del
            cliente que nos han proporcionado sólo contienen nombres completos.
        </p>

        <p>
            Por lo tanto el sistema intenta separar un nombre completo en sus
            componentes separándolos por el espacio en blanco.
        </p>

        <div class="example">

            <h3>Ejemplos</h3>

            <table concordion:execute="#result = split(#fullName)">
                <tr>
                    <th concordion:set="#fullName">Nombre Completo</th>
                    <th concordion:assertEquals="#result.firstName">Nombre</th>
                    <th concordion:assertEquals="#result.lastName">Apellido</th>
                </tr>
                <tr>
                    <td>Juan Pérez</td>
                    <td>Juan</td>
                    <td>Pérez</td>
                </tr>
                <tr>
                    <td>Felipe Reyes</td>
                    <td>Felipe</td>
                    <td>Reyes</td>
                </tr>
            </table>

        </div>
    </body>
</html>

Esta instrumentación tiene el mismo comportamiento que la del ejemplo anterior.

concordion:verifyRows

Algunas veces queremos comprobar los contenidos de una colección de resultados que devuelve el sistema. En el framework Fit usaríamos para esto una RowFixture. En Concordion usamos el comando verifyRows.

Por ejemplo, mientras escribimos una herramienta de administración de usuarios, podríamos escribir una especificación como la que sigue en la que describimos el comportamiento de la funcionalidad de búsqueda:

Especificación Original

La idea es que en el código de fijación añadamos los usuarios al sistema, realicemos una búsqueda y entonces confirmemos que en los resultados de búsqueda obtenemos los usuarios correctos (y sólo estos usuarios). Si hay usuarios de más o de menos, o no son los usuarios esperados, queremos que el test falle.

El código HTML instrumentado para esta especificación es como sigue:

<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

<h1>Correspondencias Parciales</h1>

<p>
    Las búsquedas por nombre de usuario devuelven correspondencias parciales, e.d.,
    se devuelven todos los nombres de usuario que contienen la cadena de búsqueda.
</p>

<div class="example">

    <h3>Ejemplo</h3>

    <p>Dados estos usuarios:</p>

    <table concordion:execute="setUpUser(#username)">
        <tr><th concordion:set="#username">Nombre de usuario</th></tr>
        <tr><td>john.lennon</td></tr>
        <tr><td>ringo.starr</td></tr>
        <tr><td>george.harrison</td></tr>
        <tr><td>paul.mccartney</td></tr>
    </table>

    <p>La búsqueda por "<b concordion:set="#searchString">arr</b>" devolverá:</p>

    <table concordion:verifyRows="#username : getSearchResultsFor(#searchString)">
        <tr><th concordion:assertEquals="#username">Nombres de usuario con correspondencia</th></tr>
        <tr><td>george.harrison</td></tr>
        <tr><td>ringo.starr</td></tr>
    </table>

</div>

</body>
</html>

La sintaxis del comando verifyRows es:

#loopVar : expression

Donde expression devuelve un objeto Iterable que se recorre en un orden predecible, (p.ej. una List, LinkedHashSet o un TreeSet). Y #loopVar proporciona acceso al objeto actual durante el recorrido y permite al método assertEquals comprobar su valor.

El orden de los elementos de la tabla que se verifica deben corresponder con el orden del recorrido que nos devuelve expression. Necesitamos ordenar los elementos para asegurar que están en un orden conocido y consistente. En nuestro ejemplo, estamos usando el orden alfabético ("george" antes que "ringo").

El esqueleto del código de fijación sería como sigue:

public class PartialMatchesTest extends ConcordionTestCase {

    public void setUpUser(String username) {
        // TODO: Dar de alta al usuario en el sistema
    }

    public Iterable<String> getSearchResultsFor(String searchString) {
        // TODO: Realizar la búsqueda y devolver los resultados reales
        return new ArrayList<String>();
    }
}

Si ejecutamos el test con este esqueleto obtenemos:

Faltan dos filas

Faltan dos filas porque nuestra función de búsqueda no está implementada y devuelve un conjunto vacío.

Sólo para demostrar cómo funciona, implementemos la funcionalidad dentro del código de fijación (en vez de llamar al sistema, que sería lo correcto):

public class PartialMatchesTest extends ConcordionTestCase {

    private Set<String> usernamesInSystem = new HashSet<String>();

    public void setUpUser(String username) {
        usernamesInSystem.add(username);
    }

    public Iterable<String> getSearchResultsFor(String searchString) {
        SortedSet<String> matches = new TreeSet<String>();
        for (String username : usernamesInSystem) {
            if (username.contains(searchString)) {
                matches.add(username);
            }
        }
        return matches;
    }
}

Ahora cuando ejecutamos el test obtenemos un éxito:

Éxito

Éstas son las características esenciales de Concordion y deberían ser todo lo que necesitaramos para comenzar. En la página Technique podemos encontrar consejos sobre cómo enfocar adecuadamente nuestras especificaciones.