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.