En casi todas sus charlas, Misko Hevery dice que hay una sola razón valida por la que un programador no debería escribir pruebas automatizadas del software que este desarrollando, y esta es que no sepa como hacer pruebas de software.
Toma en cuenta que por mas experimentados que podamos ser en el desarrollo de software, realizar pruebas automatizadas es una habilidad particular y si nunca la hemos ejercido no podemos esperar saber utilizarla como por arte de magia.
Considera cuales habilidades has adquirido como programador:
- Validar algoritmos
- Conocer estructuras de datos
- Escribir software en un lenguaje
- Diseñar aplicaciones en un modelo de programación
- Etcetera
Ahora pregúntate de forma sincera como hiciste para obtener el nivel de dominio que tienes actualmente:
- Cuantas horas de estudio requirió cada una de ellas?
- Cuantos libros compraste?
- Cuantas clases tomaste?
- Cuantos tutoriales buscaste en Internet?
Y ahora pregúntate, por qué creemos que hacer pruebas automatizadas es diferente?
Si alguien te entrega un sistema por primera vez y te dice «necesito que pruebes el funcionamiento del modulo de registro de usuarios de forma manual«, te sentirías cómodo en ese rol? Quizás no tanto.
Te tomaría tiempo familiarizarte con el sistema y determinar como lo vas a probar, hacer planes de que vas a probar y entender como vas a reportar lo que has descubierto; esto es, si aceptas hacer la actividad en primer lugar.
Parece mentira, pero no todos los programadores se sienten cómodos indagando el funcionamiento de software ya en producción de forma manual.
Creo que aquí se esconde el secreto de lo que ocurre, nos engañamos al confundir el proceso de escritura de software con el proceso de realizar pruebas automatizadas de software, ya que pensamos que hacer código de prueba es lo mismo que hacer código de producción, ya que comparten la idea de que es código lo que se esta haciendo; pero su propósito final determina la diferencia fundamental que muchas veces ignoramos.
Si te pido que hagas un código de producción para un sistema contable, lo primero que debes hacer es familiarizarte con ese proceso contable para entender que vas a programar, igualmente si vas a escribir código de pruebas, no solo debes familiarizarte con la tecnología de escritura de código (un framework como jUnit o TestNG en el caso de pruebas unitarias), sino también con el proceso de como realizar buenas pruebas.
Es por esto que he decidido que siguiendo las ideas de mi post anterior sobre TDD, exploremos juntos como implementar buenas pruebas.
Qué hace a una prueba automatizada buena?
Si bien, cada uno de los diferentes tipos de pruebas maneja sus propios estándares, pero comparten en general una definición base de cuales son los factores que hacen a una prueba automatizada buena
- El nivel de lectura del código
- La capacidad de mantenimiento del código
- Cómo se encuentra organizado, tanto en el proyecto como dentro del archivo fuente
- Qué esta chequeando
- Cómo maneja las dependencias del Sistema Bajo Prueba (el termino técnico es SUT por sus siglas en ingles).
Dicho esto, nosotros nos enfocaremos en el post de hoy en evaluar que hace a una prueba unitaria «buena». Recordemos primero la definición que utilizamos en nuestro post sobre TDD.
Qué es una Prueba Unitaria?
La idea de realizar pruebas unitarias viene desde los años 70, impulsadas por Kent Beck (creador de la metodología XP y uno de los padres del Movimiento Agil) en el lenguaje Smalltalk y esta demostrado que es una de las maneras mas seguras de mejorar la calidad del código del desarrollo.
Wikipedia define como prueba unitaria «a una porción de código (comúnmente un método) que que invoca otra porción de código y chequea diversas presunciones sobre el mismo; si estas presunciones están erradas, entonces la prueba ha fallado«.
Pero esta definición a mi parecer es un poco escueta, veamos entonces que mas podemos considerar sobre las pruebas unitarias para poder calificarlas como buenas.
Propiedades de una Buena Prueba Unitaria
Roy Osherove (mi autor favorito en la materia) en su excelente libro The Art of Unit Testing nos dice que no solo es importante conocer técnicamente que es una prueba unitaria sino también conocer las propiedades una buena prueba unitaria.
- Debe ser automatizada y repetible.
- Debe ser fácil de implementarse.
- Una vez que sea escrita debe mantenerse para uso a futuro.
- Cualquiera debe ser capaz de ejecutarla (a no ser que estemos desechando la funcionalidad a la que corresponde la prueba).
- Debe correrse de forma sencilla.
- Debe correr rápidamente.
Y si me permito agregar una propiedad adicional seria que el propósito de toda prueba es pasar.
Qué es una Prueba Unitaria Buena?
Conociendo ya la definición técnica y las propiedades de una buena prueba, podemos tratar de definir las pruebas unitarias.
Las pruebas unitarias buenas son las que evaluan de forma automatizada un punto especifico del código, ya sea un método, una propiedad o una función definiendo lo que esperamos sea el resultado y/o comportamiento del mismo; estas pruebas se realizan de forma aislada al resto de los programas o clases que conforman el sistema.
De igual manera, al implementarse utilizando un Framework de pruebas las mismas deberían poder crearse de forma sencilla, siendo su ejecución confiable y veloz, mientras que al mismo tiempo su código debe ser legible y mantenible.
Como punto adicional, hay una convención sobre la estructura que debe tener una buena prueba unitaria, y es lo que se conoce como el modelo «AAA«, esto viene de los términos «Arrange, Act, Assert» o «Preparar, Actuar, Asegurar» (la palabra asertar también existe en español, es sinónimo de aseverar y asegurar algo).
Es decir, en toda prueba, primero preparamos los elementos a probar, ejecutamos alguna acción y luego nos aseguramos nuestras suposiciones sean correctas.
A qué se llama Olores de Pruebas?
Cuando se dice que tenemos un «olor» en una prueba, nos referimos a un indicio, algún indicador de que quizás la prueba no sea buena o de que hay algo en el código de producción que no funciona como debería.
Es importante hacer notar, que no porque una prueba te de «olores» de un tipo, es automáticamente mala, hay ciertos compromisos a los que debemos llegar con nuestro código que en algunos casos nos llevan a hacer excepciones y concesiones transitorias; lo importante es que un olor nos hace parar por un momento y revisar una prueba para comprobar su comportamiento y entender si su código puede ser optimizado o hay alguna falla en la misma.
De igual manera, hay que destacar que nuestras pruebas deberían estar tan libre de olores como sea posible, y la única manera de lograr esto es sabiendo como reconocer estos indicios.
En mi opinión, Lasse Koskela, en su libro Effective Unit Testing, plantea la mejor separación de olores al plantearlos en 3 grandes categorías
- Olores de Legibilidad
- Olores de Mantenibilidad
- Olores de Confiabilidad
Es en el estudio de estos olores que haremos énfasis en nuestros próximos posts. Por ahora quiero enfocarme en mostrarte un ejemplo de como se ven malas pruebas unitarias y como se ven las buenas pruebas unitarias.
NOTA: Los ejemplos que veremos a continuación se encuentran desarrollados en Java utilizando el Framework JUnit.
Malas Pruebas Unitarias
Quizás en este punto te plantearas, como reconocemos una mala prueba unitaria?
Pues sencillamente, si captamos problemas de legibilidad, mantenibilidad o confiabilidad podemos empezar a sospechar que una prueba puede ser mala. Una vez que hemos pausado para revisar la prueba, podemos determinar si es solo un indicio de error o una falla en el código de prueba o en el código de producción.
NOTA: La excepción a esto es cuando una prueba falla de forma aleatoria o requiere condiciones especiales para pasar, esto es un indicador sine qua non de que hay un error en la implementación del código, ya sea de producción o de prueba.
Quiero mostrarte un terrible intento de prueba unitaria, el mismo es de mi autoría y fue sobre un proyecto en el que di mis primeros pasos de lleno en el TDD.
Veamos el código:
public class EntitiesFactoryJSONTest {
private EntitiesFactoryJSON factoryJSON;
private EntitiesFactory factory;
@Before
public void setUp() throws Exception
{
factory=new EntitiesFactory();
factoryJSON=new EntitiesFactoryJSON();
}
@Test
public void debeCrearPedidos()
{
Pedido pedido=factory.crearPedido();
PedidoJSON pedidojson=factoryJSON.parsePedido(pedido);
assertEquals(true, pedido.getHora()==pedidojson.getHora());
assertEquals(true, pedido.getFecha()==pedidojson.getFecha());
Usuario usuario=factory.crearUsuarioFinal();
usuario.setCedula("V17230971");
usuario.setNombre("jorge");
usuario.setDireccion("sanfra");
usuario.setTelefono("02617623790");
Catalogo catalogo=factory.crearCatalogo();
catalogo.setNombre("test");
ElementoCatalogo elemento1=factory.crearElementoCatalogo(catalogo);
ElementoCatalogo elemento2=factory.crearElementoCatalogo(catalogo);
ElementoCatalogo elemento3=factory.crearElementoCatalogo(catalogo);
elemento1.setPrecio(10.0f);
elemento2.setPrecio(20.0f);
elemento3.setPrecio(50.0f);
List<ElementoCatalogo> elementos=factory.listaElementosCatalogo();
elementos.add(elemento1);
elementos.add(elemento2);
elementos.add(elemento3);
pedido=factory.crearPedido(usuario, elementos);
pedidojson=factoryJSON.parsePedido(pedido);
assertEquals(usuario.getNombre(), pedidojson.getCliente().getNombre());
assertEquals(usuario.getDireccion(), pedidojson.getDireccionEntrega());
assertEquals(usuario.getTelefono(), pedidojson.getTelefonoEntrega());
assertEquals(true,pedidojson.getSubTotal()==80.0f);
}
}
Esto esta tan mal que casi me da risa al ver el código; para ilustrar la idea de que esta mal en esta prueba te pregunto: que estoy probando?
Parecería obvio por el nombre del método que estoy probando que EntitiesFactoryJSON debe ser capaz de crear pedidos, entonces
- Por qué evaluó tantas cosas con las funciones assertEquals?
- Por qué preparo tantos valores con esos elementos en particular?
- Por que involucro a EntitiesFactory en el proceso?
Hay otras preguntas que se pueden hacer pero para mantener la brevedad te las responderé todas con una sola respuesta: esta prueba esta mal.
Cada una de estas preguntas indica un olor particular, incluyendo mas que no hemos expresado; cuando una prueba presenta varios olores en conjunto, casi siempre es señal de que la prueba esta mal.
Una Prueba Unitaria Buena
Veamos por el contrario el código de una prueba unitaria que pudiésemos considerar como buena, también de mi autoría.
@RunWith(MockitoJUnitRunner.class)
public class UsuariosServiceAuthenticateUsuarioTest extends AbstractUsuarioServiceTest
{
private String testLogin;
private String testPassword;
@Before
public void setUpInstanciacion()
{
instanciacion();
testLogin=TestEntitiesFactory.VALID_LOGIN;
testPassword=TestEntitiesFactory.VALID_PASSWORD;
}
@Test
public void AuthenticateUsuario_Success_ReturnsResponseOK() throws Exception
{
when(administradorUsuariosMock.auntenticarUsuario(TestEntitiesFactory.VALID_LOGIN,TestEntitiesFactory.VALID_PASSWORD)).thenReturn(TestEntitiesFactory.validUsuario());
Response responseCheck = usuarioService.authenticateUsuario(testLogin, testPassword);
assertOK(responseCheck);
}
}
Una vez mas te pregunto: que estoy probando?
Si vemos el nombre de la prueba es AuthenticateUsuario_Success_ReturnsResponseOK()
(recordemos que la prueba es el método, no la clase), esto nos permite leer de forma clara el propósito de la prueba; cuando AuthenticateUsuario se ejecute con exito, deberíamos obtener un Response de tipo OK.
Esta es una muy buena practica de legibilidad que he adquirido de Roy Osherove y les recomiendo al momento de nombrar sus pruebas.
Quizás veas código que a primera vista parezca mas complicado que el que implementamos en lo que consideramos la prueba mala, especialmente el que esta relacionado con la herramienta Mockito, posiblemente a primera mano pensaras que el código es difícil de leer y te dispara un olor, también veras referencias a cosas no declaradas en la clase (una clase con pruebas unitarias se conoce como Test Fixture o traducido de forma muy rustica Accesorio de Pruebas), este es otro olor.
Pero con una inspección mas cercana, notaras que este fixture es una clase heredada, por lo que deben haber mas fixtures que compartan elementos con este, si tenemos muchos elementos comunes utilizar herencia es valido para simplificar el código de cada clase.
Igualmente podemos decir que el uso de Mockito, o cualquier otra herramienta de Dobles de Pruebas (o Mocks) optimiza el manejo de dependencias, permitiendo a la prueba ejecutarse de forma aislada, esto garantiza la ejecución veloz de nuestra prueba (si no estamos familiarizados con lo que hace esta herramienta, pues mas que un olor de la prueba es una habilidad pendiente por adquirirse).
De la misma manera, podemos ver que la prueba en si sigue la estructura AAA, otra señal de que la prueba es buena.
Es perfecta esta prueba? no lo creo, a pesar de que la considero una buena prueba unitaria, la verdad es que aún hay código que puede optimizarse para mejorar su calidad; y este es un punto muy importante a comprender:
- Debemos tratar nuestro código de pruebas con la misma atención y cuidado que nuestro código de producción.
Es decir, una prueba puede refactorizarse y cambiar su estructura para buscar la optimización con el tiempo, siempre y cuando sigamos garantizando que sea buena y que cumpla su función.
Esta es la señal de un buen ingeniero de software.
Con esto cerramos por ahora, en nuestros siguientes posts expandiremos la idea de los olores, el uso de Mocks, la cobertura de código y otros puntos importantes para cuando implementamos el desarrollo con pruebas unitarias.
Como siempre espero haya sido de tu agrado y recuerda que cualquier comentario es bien recibido.
Hasta la próxima.