1. Introducción

Hola a todos y bienvenidos a un nuevo post de nuestro querido blog!

Una de las principales preocupaciones de los desarrolladores es escribir código de calidad que tenga como referencia algunos de los principios de la ingeniería del software como puede ser SOLID. Lo que debemos buscar es un procedimiento o mecanismo que nos guíe en la elaboración del mismo y que además nos proporcione un conjunto de herramientas de testeo de nuestro sistema. Es aquí cuando cobra sentido BDD!. BDD (Behaviour Driven Development) es un proceso de desarrollo de software que nos obliga a determinar el comportamiento de nuestro sistema antes de tirar una línea de código. Fue desarrollado por Dan North queriendo dar respuesta a las limitaciones de la metodología TDD (Test Driven Development) ya que consideraba que existía un problema a la hora de determinar cuáles debían ser las pruebas que se debían escribir antes de iniciar el desarrollo del producto. BDD nos indica que el comportamiento de nuestro sistema se debe especificar en escenarios y éstos en steps tales como Given, When o Then.

  • Given. Indica una inicialización del escenario
  • When. Indica una acción concreta en el escenario
  • Then. Indica la respuesta esperada de la acción previa

En este post utilizaremos Cucumber como implementación de BDD.

Os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Java 8
  • Gradle 3.1
  • Junit 4.11
  • Cucumber 1.2.5
  • Spring Boot 1.5.3

2. Cucumber

Para empezar a utilizar Cucumber en nuestro poc deberemos añadir las dependencias cucumber-java y cucumber-junit en nuestro build.gradle. Además, si queremos integrarlo con spring es necesario hacer uso de cucumber-spring.

group 'com.jorgehernandezramirez.bdd'
version '1.0-SNAPSHOT'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile group: 'info.cukes', name: 'cucumber-java', version: '1.2.5'
    testCompile group: 'info.cukes', name: 'cucumber-junit', version: '1.2.5'
    testCompile group: 'info.cukes', name: 'cucumber-spring', version: '1.2.5'
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Los escenarios en cucumber se especifican en los ficheros .feature que se deben encontrar dentro del classpath de la aplicación. Para nuestra prueba de concepto creamos el fichero user.feature que contendrá dos escenarios.

  • Modificación salario usuario. Escenario donde incrementamos el salario de un usuario un 3% esperando la actualización correspondiente.
  • Obtener número de usuarios con rol USER. Escenario donde buscamos todos los usuarios con rol USER esperando que en el sistema devuelva dos registros.
Feature: Gestión usuarios

  Scenario: Modificación salario usuario
    Given dado los siguientes usuarios en el sistema
      | id  | name        | rol      | salary |
      | 1   | Jorge       | USER   | 18000   |
      | 2   | Jose        | USER   | 20000   |
      | 3   | Admin       | ADMIN  | 40000   |
    When se sube el salario del empleado '1' un 3%
    Then el salario del empleado '1' es 18540

  Scenario: Obtener número de usuarios con rol USER
    Given dado los siguientes usuarios en el sistema
      | id  | name        | rol    | salary |
      | 1   | Jorge       | USER   | 18000   |
      | 2   | Jose        | USER   | 20000   |
      | 3   | Admin       | ADMIN  | 40000   |
    When se obtiene los usuarios con rol USER
    Then el número de usuarios obtenidos es 2

Creamos las clases necesarias para definir una api de usuarios que nos permita pasar con éxito los escenarios definidos. Se muestra el Dto de usuarios.

package com.jorgehernandezramirez.bdd.cucumber.dto;

public class UserDto {

    private String id;

    private String name;

    private String rol;

    private Double salary;

    public UserDto(){
        super();
    }

    public UserDto(String id, String name, String rol, Double salary) {
        this.id = id;
        this.name = name;
        this.rol = rol;
        this.salary = salary;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRol() {
        return rol;
    }

    public void setRol(String rol) {
        this.rol = rol;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        UserDto userDto = (UserDto) o;

        if (id != null ? !id.equals(userDto.id) : userDto.id != null) return false;
        if (name != null ? !name.equals(userDto.name) : userDto.name != null) return false;
        if (rol != null ? !rol.equals(userDto.rol) : userDto.rol != null) return false;
        return salary != null ? salary.equals(userDto.salary) : userDto.salary == null;

    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (rol != null ? rol.hashCode() : 0);
        result = 31 * result + (salary != null ? salary.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "UserDto{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", rol='" + rol + '\'' +
                ", salary=" + salary +
                '}';
    }
}

Api de usuarios.

package com.jorgehernandezramirez.bdd.cucumber.service;

import com.jorgehernandezramirez.bdd.cucumber.dto.UserDto;

import java.util.List;

/**
 * Api de usuarios
 */
public interface IUserService {

    /**
     * Método que devuelve los usuarios del sistema
     * @return
     */
    List<UserDto> getUsers();

    /**
     * Método que devuelve un usuario del sistema
     * @param id
     * @return
     */
    UserDto getUser(String id);

    /**
     * Método que guarda un usuario en el sistema
     * @param userDto
     */
    void save(UserDto userDto);

    /**
     * Método que actualiza un usuario en el sistema
     * @param userDto
     */
    void update(UserDto userDto);
}

Cada paso definido en el fichero anterior debe tener un disparador en una clase Java que será cargada por cucumber en el momento del arranque de las pruebas. Utilizamos las anotaciones @Given, @When, @Then para indicar la expresión regular que se debe asociar al step definido en el fichero de escenarios. En UpdateSalarySteps.java definimos los steps correspondientes al escenario de actualización del salario.

package com.jorgehernandezramirez.bdd.cucumber.steps;

import com.jorgehernandezramirez.bdd.cucumber.configuration.UserConfiguration;
import com.jorgehernandezramirez.bdd.cucumber.dto.UserDto;
import com.jorgehernandezramirez.bdd.cucumber.service.IUserService;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;

import java.util.List;

import static org.junit.Assert.assertEquals;

@ContextConfiguration(classes = UserConfiguration.class)
public class UpdateSalarySteps {

    @Autowired
    private IUserService userService;

    @Given("^dado los siguientes usuarios en el sistema$")
    public void inicializacionDeusuarios(final List<UserDto> userDtoList) {
        userDtoList.forEach(userDto -> {
            userService.save(userDto);
        });
    }

    @When("^se sube el salario del empleado '(\\d+)' un (\\d+)%$")
    public void incrementarElSalario(final Integer userId, final Integer porcentaje){
        final UserDto userDto = userService.getUser(String.valueOf(userId));
        userDto.setSalary(userDto.getSalary() + userDto.getSalary() * porcentaje / 100D);
        userService.update(userDto);
    }

    @Then("^el salario del empleado '(\\d+)' es (\\d+)")
    public void verifyAmountOfBooksFound(final Integer userId, final Double salario) {
        assertEquals(userService.getUser(String.valueOf(userId)).getSalary(),
                salario);
    }
}

Mientras que en FilterUserSteps.java definimos los steps correspondientes al filtrado de usuarios.

package com.jorgehernandezramirez.bdd.cucumber.steps;

import com.jorgehernandezramirez.bdd.cucumber.configuration.UserConfiguration;
import com.jorgehernandezramirez.bdd.cucumber.dto.UserDto;
import com.jorgehernandezramirez.bdd.cucumber.service.IUserService;
import com.jorgehernandezramirez.bdd.cucumber.service.InMemmoryUserService;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;

import java.util.List;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;

@ContextConfiguration(classes = UserConfiguration.class)
public class FilterUserSteps {

    @Autowired
    private IUserService userService;

    private List<UserDto> usersWithRolUser;

    @When("^se obtiene los usuarios con rol (.*)")
    public void debeObtenerTodosLosUsuariosConRol(final String rol){
        usersWithRolUser = userService.getUsers().stream().filter(userDto -> rol.equals(userDto.getRol())).collect(Collectors.toList());
    }

    @Then("^el número de usuarios obtenidos es (\\d+)")
    public void debeValidarElNumeroDeUsuariosObtenidos(final Integer numeroDeUsuarios) {
        assertEquals(Integer.valueOf(usersWithRolUser.size()),
                Integer.valueOf(numeroDeUsuarios));
    }
}

La inyección de dependencias será resuelta por Spring por lo que deberemos utilizar la anotación @ContextConfiguration que nos permitirá indicar los ficheros de configuración que nos ayudarán a crear el contexto. Además definimos la implementación InMemmoryUserService de nuestra api de usuarios.

package com.jorgehernandezramirez.bdd.cucumber.configuration;

import com.jorgehernandezramirez.bdd.cucumber.service.IUserService;
import com.jorgehernandezramirez.bdd.cucumber.service.InMemmoryUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfiguration {

    public UserConfiguration(){
        //Para Spring
    }

    @Bean
    public IUserService buildUserService(){
        return new InMemmoryUserService();
    }
}
package com.jorgehernandezramirez.bdd.cucumber.service;

import com.jorgehernandezramirez.bdd.cucumber.dto.UserDto;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class InMemmoryUserService implements IUserService {

    private static final List<UserDto> USERS = new ArrayList<UserDto>();

    public InMemmoryUserService(){
        super();
    }

    @Override
    public List<UserDto> getUsers() {
        return USERS;
    }

    @Override
    public UserDto getUser(final String id) {
        final Optional<UserDto> userDtoOptional = USERS.stream().filter(userDto -> id.equals(userDto.getId())).findFirst();
        if(userDtoOptional.isPresent()){
            return userDtoOptional.get();
        }
        return null;
    }

    @Override
    public void save(UserDto userDto) {
        if(!userExists(userDto.getId())) {
            USERS.add(userDto);
        }
    }

    @Override
    public void update(final UserDto userDto) {
        final UserDto userToUpdate = getUser(userDto.getId());
        if(userToUpdate != null){
            userToUpdate.setName(userDto.getName());
            userToUpdate.setSalary(userDto.getSalary());
        }
    }

    private Boolean userExists(final String id){
        return getUser(id) != null;
    }
}

Finalmente para lanzar la ejecución de los escenarios en Junit basta con utilizar la anotación @RunWith(Cucumber.class) sobre la clase principal del test.

package com.jorgehernandezramirez.bdd.cucumber;

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(features={"src/test/resources/feature"}, glue = {"com.jorgehernandezramirez.bdd.cucumber.steps"})
public class UserTest {
}

Además utilizamos el atributo feature sobre la anotación @CucumberOptions para indicar dónde se encuentran los ficheros que definen los escenarios dentro del classpath, mientras que utilizamos el atributo glue para indicar el package de las clases donde se encuentran la definiciones de los steps en Java.

3. Resultado de la ejecución de los test