¡No te pierdas ninguna publicación! Suscríbete a The Softtek Blog
Esta es una buena pregunta que nos podemos hacer todos los implicados en un proyecto de desarrollo de software y en especial los desarrolladores. Debemos ser los primeros interesados en hacer test unitarios y no sólo por el hecho de hacerlos y ser un indicativo más de cara a la galería. Se deben hacer test unitarios de igual forma que si se tratase código de producción.
El desarrollo de software, al igual que otros productos que utilizamos diariamente, debe ser testeado. En la cocina por ejemplo, ¿os imagináis que los platos que nos sirven no los pruebe antes el chef?; en automoción todas las piezas de un coche deben pasar pruebas de calidad; o cuando nos compramos un coche, esperamos que todo funcione perfectamente; y así innumerables ejemplos. En todos los sectores las pruebas tienen un papel muy importante. Y, por tanto, no puede ser menos importante en nuestro sector. Nos enfrentamos a usuarios de distintas generaciones, múltiples dispositivos y, si estamos en el mundo web, múltiples exploradores. Además los usuarios queremos aplicaciones rápidas, seguras, usables, etc.
Una vez que el equipo de desarrollo de software asume el hacer test como una parte más de su trabajo, es muy importante ser mentor al resto de intervinientes en el proyecto.
Un sistema con test unitarios será más fácil cambiarlo con la seguridad de que no vamos a romper nada. Nos permitirá realizar tareas de refactoring sin miedo y buscar posibles bugs de forma más rápida. Además al hacer test, se detectarán malas prácticas de diseño y asumimos estas mejoras en etapas tempranas del desarrollo, lo que implica una mejor calidad del sistema.
Aunque puede parecer extraño no hace muchos años, e incluso en la actualidad, hay proyectos de software que se desarrollan y se suben a producción sin ningún test. Bueno, esto no es cierto del todo, siempre hay algún responsable del proyecto que realiza “sus pruebas” y da el OK al software. Está bien realizar este tipo de pruebas pero no te aseguran que el software es robusto.
En muchas ocasiones se ha arreglado el software por una incidencia y a la vez se han generado muchas más por otro sitio. ¿Cómo evitar estos derroches de trabajo y horas sin disfrutar con tus amigos o familia?
Todos debemos ser conscientes (desde el programador junior hasta los gerentes de grandes cuentas) de la importancia de realizar testing. ¿Qué tipo de testing? Cuanto más, mejor, pero siempre con sentido común y no hacer test simplemente para mejorar los números del Sonar de cara a la galería.
Si eres desarrollador Java seguro que estas excepciones las has vistos en algún log de producción:
java.lang.NullPointerException, java.lang.ArrayIndexOutOfBoundsException
Quizás con unos buenos test unitarios se hubieran podido evitar.
Al igual que éstas, que son más complejas de analizar:
Con unas buenas pruebas de carga también se hubieran podido detectar.
Este tipo de excepciones son más difíciles de detectar con testing ya que dependen de la versión de la JVM/librerías. Para asegurarse de detectarlas a tiempo es importante que la infraestructura, arquitectura y JVM dispongan de las mismas versiones en todos los entornos.
Pero como dice Edsger Dijkstra:
"El testing puede probar la presencia de errores pero no la ausencia de ellos"
Ya hemos mencionado algunos tipos de test: unitarios y de carga. Hay varios tipos de test, incluso se podrían añadir alguno más que vuestros sistema requiera:
Los tests unitarios prueban las funcionalidades implementadas en el SUT (System Under Test). Si somos desarrolladores Java, para nosotros el SUT será la clase Java.
Los tests unitarios deben cumplir las siguientes características:
Además podemos añadir estos dos puntos más:
Los tests unitarios tienen la siguiente estructura:
JUnit es un framework Java para implementar test en Java. Se basa en anotaciones:
Las condiciones de aceptación del test se implementa con los asserts. Los más comunes son los siguientes:
Además existen otras posibilidades más avanzadas:
En este último año ha salido la versión 5 de JUnit. En el siguiente enlace podéis encontrar las nuevas características. Mencionar que es compatible con las versiones anteriores ya que ha mantenido la paquetería org.junit para la implementación anterior y para JUnit 5 org.junit.jupiter.
Mockito es una librería Java que permite simular el comportamiento de una clase de forma dinámica. De esta forma nos aislamos de las dependencias con otras clases y sólo testeamos la funcionalidad concreta que queremos.
La simulación del comportamiento de una clase se hace mediante los “dobles” que pueden ser de distintos tipos:
A continuación se muestra un ejemplo práctico con test unitarios y de integración entre dos componentes. Se trata de resolver una ecuación de primer grado del tipo: ax + b = c, donde a, b y c son números enteros. Por lo tanto la solución a dicha ecuación es x = (c - b) / a
En el código "a" lo llamamos parte1, "b" lo llamamos parte2, "+" o "-" es el operador y "c" es la parte3.
El proyecto tienes 2 clases: EcuacionPrimerGrado con el método que tiene la fórmula para resolver la ecuación y Parseador, que se encarga de realizar el parseo de la cadena con la ecuación.
package com.softtek.ecuacion;
/**
* Ecuacion de primer grado
* Solucion: x = (c - b) / a
* es decir: x = (parte3 - parte2)/parte1
*/
public class EcuacionPrimerGrado {
private Parseador parseador = new Parseador();
public double obtenerResultado(final String ecuacion) {
int parte1 = parseador.obtenerParte1(ecuacion);
int parte2 = parseador.obtenerParte2(ecuacion);
int parte3 = parseador.obtenerParte3(ecuacion);
double resultado = Double.valueOf((parte3 - parte2)) / Double.valueOf(parte1);
return resultado;
}
}
El Parseador tiene el siguiente código:
package com.softtek.ecuacion;
public class Parseador {
public int obtenerParte1(final String ecuacion) {
String[] partes1 = obtenerPartes12(ecuacion);
String parte1 = partes1[0].trim();
return Integer.valueOf(parte1.substring(0, parte1.length() - 1));
}
public int obtenerParte2(final String ecuacion) {
String[] partes1 = obtenerPartes12(ecuacion);
String parte2 = partes1[1].trim();
String operador = obtenerOperador(ecuacion);
if ("-".equals(operador)) {
return Integer.valueOf(parte2) * (-1);
}
return Integer.valueOf(parte2);
}
public String obtenerOperador(final String ecuacion) {
if (ecuacion.indexOf('+') > 0) {
return "+";
} else {
return "-";
}
}
public int obtenerParte3(final String ecuacion) {
String[] partesEcuacion = ecuacion.split("=");
return Integer.valueOf(partesEcuacion[1].trim());
}
private String[] obtenerPartes12(final String ecuacion) {
String[] partesEcuacion = ecuacion.split("=");
String operador = obtenerOperador(ecuacion);
String[] partes1 = partesEcuacion[0].split("\\" + operador);
return partes1;
}
}
A continuación se muestra los tests unitarios del parseador. El SUT es la clase Parseador y no tiene ninguna dependencia con ningún otro componente.
package com.softtek.ecuacion;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class ParseadorTest {
private final Parseador parseador = new Parseador();
@Test
public void obtenerParte1Unidades() {
String ecuacion1 = "2x - 1 = 0";
int resultado = parseador.obtenerParte1(ecuacion1);
assertEquals(2, resultado);
}
@Test
public void obtenerParte2Suma() {
String ecuacion1 = "2x + 1 = 0";
int resultado = parseador.obtenerParte2(ecuacion1);
assertEquals(1, resultado);
}
@Test
public void obtenerParte3Positivo() {
String ecuacion1 = "2x + 1 = 3";
int resultado = parseador.obtenerParte3(ecuacion1);
assertEquals(3, resultado);
}
@Test
public void obtenerOperadorSuma() {
String ecuacion2 = "2x + 1 = 0";
String operador = parseador.obtenerOperador(ecuacion2);
assertEquals("+", operador);
}
}
El resultado de la ejecución de los tests se visualiza de la siguiente forma con Eclipse:
A continuación, se muestra un ejemplo de un test de integración donde se verifica la interacción del componente EcuacionPrimerGrado y el Parseador. Los test comprueban que el resultado final de la ecuación es correcto.
package com.softtek.ecuacion;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class EcuacionPrimerGradoIntegrationTest {
EcuacionPrimerGrado ecuacion = new EcuacionPrimerGrado();
@Test
public void solucionaEcuacionConMenos() {
Double result = ecuacion.obtenerResultado("2x - 1 = 0");
Double valueExpected = 0.5;
assertEquals(valueExpected, result);
}
@Test
public void solucionaEcuacionConMas() {
Double result = ecuacion.obtenerResultado("2x + 1 = 0");
Double valueExpected = -0.5;
assertEquals(valueExpected, result);
}
@Test
public void solucionaEcuacionConParte3Mayor0() {
Double result = ecuacion.obtenerResultado("2x + 1 = 10");
Double valueExpected = 4.5;
assertEquals(valueExpected, result);
}
}
Ahora, si sólamente quisiéramos probar de forma unitaria el método obtenerResultado de la clase EcuacionPrimerGrado debemos hacer uso de los “dobles”. Pensad en implementar estos dobles vosotros mismos sin hacer uso de ningún framework. Seguramente ya estáis pensando en crear una interfaz para la clase Parseador y crear métodos que permitan hacer la sustitución por un objeto fake...vamos, que se complica un poco.
Pues para esto podemos hacer uso de Mockito. Con @InjectMocks establecemos el objeto sobre el cual se realizará la inyección de los objetos marcados con @Mock, es necesario inicializar estos mocks con MockitoAnnotations.initMocks(this); en un método de inicialización con @Before. Para establecer comportamientos del mock Parseador se utiliza when, antes de realizar la ejecución del test. A continuación podéis ver el ejemplo:
package com.softtek.ecuacion;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class EcuacionPrimerGradoMockitoTest {
@InjectMocks
private EcuacionPrimerGrado ecuacionPrimerGrado;
@Mock
private Parseador parseador;
@Before
public void inicializaMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void solucionaEcuacionConMenos() {
String ecuacion = "2x - 1 = 0";
when(parseador.obtenerParte1(ecuacion)).thenReturn(2);
when(parseador.obtenerParte2(ecuacion)).thenReturn(-1);
when(parseador.obtenerParte3(ecuacion)).thenReturn(0);
Double result = ecuacionPrimerGrado.obtenerResultado(ecuacion);
Double valueExpected = 0.5;
assertEquals(valueExpected, result);
}
@Test
public void solucionaEcuacionConMas() {
String ecuacion = "2x + 1 = 0";
when(parseador.obtenerParte1(ecuacion)).thenReturn(2);
when(parseador.obtenerParte2(ecuacion)).thenReturn(1);
when(parseador.obtenerParte3(ecuacion)).thenReturn(0);
Double result = ecuacionPrimerGrado.obtenerResultado(ecuacion);
Double valueExpected = -0.5;
assertEquals(valueExpected, result);
}
}
Debemos asegurar la calidad de cualquier producto, también si es un producto de software. No debemos subestimar el plan de pruebas ni escatimar el tiempo que le dedicamos. Como profesionales debemos ofrecer un software robusto, libre de errores y que sea mantenible.
Todo el código de este post lo podéis encontrar mi perfil de github, en el proyecto practicaTesting.