¿Construcción y ejecución de test de aceptación? Concordion es tu amigo

Después de estar trabajando en varios proyectos con Concordion, un framework BDD, ha llegado el momento de hacer balance. En mi caso, considero que es un framework que me ha resultado bastante útil.

¿Por qué? Vamos a verlo a continuación. Vamos a ver en qué consiste trabajar con BDD, diferentes frameworks que aplican esta forma de trabajo y un caso práctico con una integración completa con Spring Boot, JPA, una BBDD en memoria H2 y el propio Concordion. ¿Empezamos?

Tal y como ya vimos en mi post acerca de TDD, teníamos el problema de que TDD no era efectivo para las pruebas integradas. Cito tal y como lo describí:

“La principal desventaja que veo a esta metodología es que no es válida (al menos bajo mi punto de vista) para test integrados, ya que necesitamos conocer los datos del repositorio y verificar que el contenido es el esperado después de realizar una transacción (o un rollback en su defecto).

Lo cual, al final, requiere tener especial cuidado y un sistema de gestión para un BBDD (aunque sea en memoria, que sería lo ideal).

Para este tipo de test integrados o funcionales hay frameworks como Concordion que ofrecen soluciones interesantes”.

Vamos a ver en qué consiste.

¿Qué es BDD?

BDD es el acrónimo de Behavior-Driven Development o lo que es lo mismo, desarrollo orientado al comportamiento. Este escenario lo que busca es un lenguaje ubicuo entre desarrollo y negocio, es decir, un lenguaje común en el que ambos interlocutores entiendan lo mismo (esto se puede complementar con un enfoque DDD, o lo que es lo mismo, diseño orientado al dominio).

Para que este marco de trabajo sea efectivo y beneficioso, necesitamos que los criterios de aceptación de las historias, o del desarrollo que vamos a llevar a cabo, estén bien definidos.

Es decir, debemos de definir cada casuística de la forma más exhaustiva posible para definir las entradas a los servicios y el resultado para esas casuísticas.

En un entorno de escenarios esto se basa en:

Given [initial context], when [event occurs], then [ensure some outcomes].”

Es decir, para cada caso de prueba vamos a definir un contexto, una acción y un resultado:

  • El contexto nos da las precondiciones que se tienen que dar para que se cumpla la casuística que se va a probar.
  • La acción es lo que vamos a probar.
  • El resultado es la postcondición después de ejecutar la acción en función de un contexto.

Hay diferentes frameworks que explotan esta forma de trabajo:

Cada framework tiene sus particularidades y, desde mi punto de vista, no prevalece ninguno sobre el resto, cada cual ofrece buenas soluciones (al menos JBehave y Cucumber, los otros dos frameworks no he trabajado con ellos, pero por si os interesa, gozan de cierta popularidad).

En nuestro caso, nos vamos a centrar en Concordion.

Concordion

Concordion es un Test Runner que puede invocar al código de la aplicación directamente como si estuviera en un servidor de aplicaciones, de tal manera que podemos probar la integración de los diferentes componentes, repositorios y controladores de la aplicación.

También puede manejar las interfaces de la aplicación desplegada.

El framework, hablando a alto nivel, al final es un HTML con una sintaxis especial, una clase de test Fixture que inyectará el componente a probar y una serie de dependencias y configuración para que tengamos resultados esperados.

Dentro de Concordion hay diversas posibilidades que nos permite el framework para obtener resultados de todo tipo. Por ejemplo, podríamos hacer una inserción en base de datos y comprobar el número de registros generados con un “verifyRows”.

Lo primero que tenemos que tener claro son las posibles entradas que puede tener nuestro Fixture, ya que Concordion solo maneja una serie de tipos de entrada como:

  • Numeric types (int, long, float, double, decimal)
  • string
  • bool

Cuando acaba la ejecución puede devolver un objeto o tipos primitivos. En caso de ser un objeto, el resultado se puede mapear como un objeto tipo mapa. Por ejemplo:

Imaginemos un método donde tenemos un rango de código postal con este formato “28003-28600”. Vamos a realizar el split y devolver el resultado en un mapa que luego podemos verificar en el HTML:

public Map split(String codePostalRange) {
    String[] postalCodes= codePostalRange.split("-");
    Map<String, String> results = new HashMap<String, String>();
    results.put("codePostalInit", postalCodes[0]);
    results.put("codePostalEnd", postalCodes[1]);
    return results;
}

Otra posibilidad es mapear el resultado en un objeto propio de Concordion llamado MultiValueResult.

  public MultiValueResult split(String codePostalRange) {
        String[] postalCodes= codePostalRange.split(" ");
        return new MultiValueResult()
                .with("codePostalInit", postalCodes[0])
                .with("codePostalEnd", postalCodes[1]);
    }

Por otro lado, también podemos especificar con una serie de anotaciones ciertas condiciones que se tienen que dar en las fases previas y posteriores a la ejecución del test:

Estos son los conocimientos más básicos y necesarios que debemos tener para empezar con nuestro ejemplo práctico.

Pero si estáis interesados en las diferentes posibilidades que nos permite el framework podemos: o bien visitar la documentación oficial de Concordion o bien en esta web de tutoriales.

De todos modos vamos a ofrecer una pequeña explicación de estas fases. Vamos a asumir que un “example” es igual a un “caso de aceptación”:

  1. Example. Se anota un fixture con @BeforeExample para invocarlo antes de cada caso de aceptación, o @AfterExample para invocarlo después de cada caso de aceptación.
  2. Specification. Se anota un fixture con @BeforeSpecification para invocarlo antes de ejecutar cualquiera de los casos de aceptación, o @AfterSpecification para invocarlo después de ejecutar todos los casos de aceptación.
  3. Suite. Se anota un fixture con @BeforeSuite para invocarlo antes de ejecutar cualquiera de las especificaciones o @AfterSuite para invocarlo después de ejecutar todas las especificaciones.

Por ejemplo, si ejecutamos un caso solo con las anotaciones example y suite podríamos tener algo como esto:

Vamos a pasar con el caso práctico, que seguro os resulta mucho más interesante.

Caso práctico

Hemos creado un proyecto Spring-boot y Spring-data con JPA donde hemos diseñado diversas capas en la aplicación:

  • Controller. Es el controller propiamente dicho que va actuar de fachada para el servicio Rest.
  • Service. Son las clases que van a comprobar si los parámetros de entrada cumplen los requisitos, es decir, comprueba que los parámetros de entrada son correctos.
  • Component. Capa que trata la lógica de negocio. Esto es lo que vamos a verificar desde Concordion, junto con sus repositorios asociados, ya que las capas superiores nos aportan poco a nivel de pruebas de negocio.
  • Repository. Capa que actúa como interface Spring Data y donde realizamos una serie de NamedQueries para obtener los resultados que deseamos en base a las consultas que queremos hacer.

Ahora vamos a entrar en configuración pura y dura. Vamos paso por paso para ver qué vamos a necesitar:

pom.xml

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.19.1</version>
  <configuration>
     <systemPropertyVariables>
        <concordion.output.dir>target/concordion</concordion.output.dir>
     </systemPropertyVariables>
     <includes>
        <include>**/*Fixture.java</include>
     </includes>
  </configuration>
</plugin>


<dependency>
  <groupId>org.concordion</groupId>
  <artifactId>concordion</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.chiknrice</groupId>
  <artifactId>concordion-spring-runner</artifactId>
  <version>0.0.1</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>

Importamos Concordion, una extensión que se integra con Spring (hay más maneras de configurarlo sin necesidad de esta dependencia, pero añade complejidad a la configuración) y h2 como base de datos en memoria.

También configuramos el plugin surefire de maven, para que cuando realicemos un install etc deje un informe con los resultados de la ejecución.

A este nivel src/test/resources, necesitamos un application.properties para declarar el datasource que vamos a utilizar para la prueba.

application.properties

# H2
spring.h2.console.enabled=true
spring.h2.console.path=/h2
# Datasource
spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Al mismo nivel, vamos a poner un import.sql que es ejecutado de forma automática por spring-boot.

import.sql

insert into feature (feature_id, code) VALUES ('44230090','No voluminoso');
insert into sku(sku_id, ref,validated) VALUES ('671888001','001014861100841',1);
insert into override_sku (sku_id, integration_ref,voluminous,company) VALUES ('671888001',1,'44230090','001');
insert into checking_restriction (id, amount) VALUES ('10002',20);

En la ruta src/test/resources/ creamos un nuevo directorio llamado “concordion”, donde crearemos el HTML.

ValidateRestriction.html

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


<h1>Validacion de restricciones</h1>





<table concordion:execute="#result = validateRestriction(#ref,#countryCode,#postalCode,#paymentGroup)">
   
<tr>
       
<th concordion:set="#ref">Referencia del producto</th>

       
<th concordion:set="#countryCode">Codigo de país</th>

       
<th concordion:set="#postalCode">Codigo postal</th>

       
<th concordion:set="#paymentGroup">Tipo de pago</th>

       
<th concordion:assert-equals="#result">Resultado</th>

   </tr>

   
<tr>
       
<td>001014861100841</td>

       
<td>011</td>

       
<td>28500</td>

       
<td>006</td>

       
<td>false</td>

   </tr>

   
<tr>
       
<td>001014861100842</td>

       
<td>011</td>

       
<td>28500</td>

       
<td>904</td>

       
<td>false</td>

   </tr>

   
<tr>
       
<td>001014861100841</td>

       
<td>011</td>

       
<td>28500</td>

       
<td>904</td>

       
<td>true</td>

   </tr>

</table>




</body>
</html>

En src/test/java/es/example creamos el Application.java para los test integrados.

BootApplicationTest.java

package es.example;

import es.example.BootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;

/**
* Created by raulmartinez on 16/06/2017.
*/
@SpringBootApplication
@ComponentScan("es.example")
public class BootApplicationTest {

   public static void main(String[] args) {
       ApplicationContext ctx = SpringApplication.run(BootApplicationTest.class, args);
   }
}

Esto ha sido en cuanto a nivel de configuración y preparación previa. Ahora entramos en materia de lo que vamos a probar con los requerimientos de negocio y la aplicación del Fixture.

El componente ValidateRestrictionComponent comprueba si para una referencia concreta se cumplen una serie de reglas que supondría tener una restricción. Comprueba que el tipo de pago es “904”, si viene cualquier otro nos da un false. Posteriormente comprueba el sku a partir de una referencia.

Si la consulta nos retorna un objeto Sku se comprueba el precio de dicho sku. Si el precio supera lo establecido en otro repositorio (RestrictionLimitRepository, que solo va a tener un registro), entonces hay una restricción.

Aquí tenemos el código que establece la lógica de negocio.

/**
* Constants
**/

public static final String PAYMENT_METHOD_PERMITTED="904";



/**
* Method that calculate if we have restriction of money laundering
*
* @param BeanRequest
* @return true if we have a restriction
*/
public boolean hasRestrictions(BeanRequest beanRequest){

   boolean hasRestrictions=false;
   if(PAYMENT_METHOD_PERMITTED.equalsIgnoreCase(beanRequest.getPaymentGroupType())){

       Sku sku= calculateSkuPropertiesComponent.getSku(beanRequest.getRef());
       if(sku!=null){
           
          Price price= getPrice(sku);
           if(price!=null){
               
               List<RestrictionLimit> restrictionLimitList=restrictionLimitRepository.findAll();
               if(restrictionLimitList!=null && !restrictionLimitList.isEmpty()) {
              

                   if (price.getPrice().doubleValue() > restrictionLimitList.get(0).getAmount().doubleValue()) {
                      
                       hasRestrictions = true;
                   }
               }
           }
       }
   }

   return hasRestrictions;
}

Finalmente, en src/test/java/concordion definimos la clase ValidateRestrictionFixture:

/**
* Created by raulmartinez on 14/06/2017.
*/
@RunWith(SpringifiedConcordionRunner.class)
@ContextConfiguration(classes = { BootApplicationTest.class })
public class ValidateRestrictionFixture {

   @Autowired
   private  ValidateRestrictionComponent validateRestrictionComponent;

   public boolean validateRestriction(String final ref,String final countryCode,String final postalCode, String final paymentGroup) throws SQLException {
           BeanRequest request= new BeanRequest ();
           request.setRef(ref);
           request.setCountryCode(countryCode);
           request.setPostalCode(postalCode);
           request.setPaymentGroupType(paymentGroup);
           return validateRestrictionComponent.hasRestrictions(request);
       }

}

Vamos a ir desgranando las anotaciones:

  • @RunWith(SpringifiedConcordionRunner.class). Es una implementación de Concordion (el normal, sin tener Spring integrado es ConcordionRunner, lo cual conlleva una serie de configuración adicional con un xml application-context.xml etc…, que añade complejidad) integrado con Spring de forma transparente.
  • @ContextConfiguration(classes = { BootApplicationTest.class }). Define la clase que va a establecer la configuración de Spring Boot y qué paquetería se va a comprobar para obtener los componentes y levantarlos en el contexto.

En el HTML que hemos definido previamente hemos visto que hemos definido 3 casos de aceptación:

  1. Entra una referencia cuyo tipo de pago es 006, por tanto nos devolverá un false, no hay restricción.
  2. Entra una referencia que no está en las tablas, por tanto nos devolverá un false, no hay restricción (si nos definieran que en tal caso debiera aparecer un mensaje de error podríamos haberlo hecho)
  3. Entra una referencia cuya forma de pago es 904, está en las tablas y cuyo importe supera el límite establecido en la tabla “checking_restriction”, tiene restricción y por tanto nos devuelve un true)

Cuando ejecutamos los test desde los distintos ciclos de vida de construcción de Maven nos aparecería una informe de este tipo:

Conclusiones

Como hemos visto, Concordion ofrece un sistema de pruebas bastante sencillo, flexible y suficientemente potente como para asegurar que nuestro sistema desarrollado es robusto (siempre y cuando tengamos una batería de pruebas acorde).

A nivel general, estas pruebas de aceptación nos proporcionan, respecto a las pruebas unitarias, un mayor nivel de cobertura en los test, más conocimiento, un lenguaje común entre desarrollo y negocio, la prueba de que nuestras distintas capas se están integrando correctamente y valor añadido al desarrollo.

Quizás lo más tedioso es la preparación de la batería de datos congruente para las pruebas. Esto es necesario para aislar posibles operaciones (en este caso solo hemos hecho consultas, pero se puede probar un CRUD) sobre datos en entornos reales.

Cuando el sistema es grande, hay que idear una buena manera de que ese conjunto de datos sea mantenible.

Con esto espero haberos ayudado con este framework y haber podido demostrar su utilidad. Como hemos visto es un sistema ligero, flexible y muy amigable visualmente, ¡así que no tenéis excusa para no utilizarlo!

Ingeniero Informático con 11 años de experiencia en el desarrollo de aplicaciones web en entornos J2EE. Después de 8 años en Indra, donde trabajó en proyectos para el Ministerio de Educación, DGT (Dirección General de Tráfico), RFEF (Real Federación Española de Fútbol) y SELAE (Loterías y Apuestas del Estado), vio en Paradigma una oportunidad de seguir creciendo. Con gran experiencia en frameworks Spring (Spring 4, Spring Boot, Spring Webflow, Spring Data, etc.), actualmente inmerso en proyectos de eCommerce con ATG 10.2 para El Corte Inglés.

Ver toda la actividad de Raúl Martínez

Escribe un comentario