Si hay un herramienta que para mi ha marcado una diferencia sustancial a la hora de mejorar la calidad de mi código es sin duda los test unitarios. En su día fueron un descubrimiento, aunque me costó entenderlos y empezar a aplicarlos, ahora me cuesta programar sin usarlos, no porque no sea capaz de programar sin usarlos, si no porque echo en falta todas las ventajas que ofrecen.
Antes de entrar en harina, voy a intentar hacer una pequeña explicación de que son.

Los test unitarios en algunos lenguajes (como en java) se llaman asserts, que se puede traducir como «afirmación», y eso es lo que hacen: afirmar que algo toma un valor concreto.
Si la afirmación es correcta, el test se pasa (se suele marcar en verde) y si no lo es, el test no se pasa (en rojo).
Afirmo que mi coche es azul: assert(myCar.getColor()).equal(«blue»)
Si el coche es azul el test se pasa, si no, no. Sencillo ¿verdad?

¿De que sirve esto? ¿Y que beneficios tiene?

Si estoy desarrollando una aplicación y quiero probar que hace lo que debe, puedo simplemente ejecutarla y ver el resultado,. Pero según se va añadiendo complejidad a la aplicación se hace más pesado probar todas las opciones, o preparar el escenario para probar el caso concreto que queremos probar. ¿A quien no le ha pasado que solucionando un bug hemos roto algo?
Aquí es donde entran en juego los test unitarios y donde demuestran todo su potencial.

Imagina que puedes probar todas esas opciones pulsando un botón y que puedes ver si has roto algo con ese cambio que has hecho. Eso es justo para lo que sirve.
Ahora que ya te he convencido para usarlo (no conozco a ningún programador en su sano juicio que después de ofrecerle una herramienta así no te diga que la quiere) vamos a ver los inconvenientes que tiene y como solventarlos con un sencillo ejemplo:

Tenemos una aplicación para crear usuarios con 2 niveles de acceso (normal y avanzado) con 3 campos: nombre, mail y nº de empleado. El usuario normal tiene que rellenar solo el nombre y el mail, pero el usuario avanzado también tiene que tener informado también el nº de empleado. Podemos probarla de la forma clásica: entramos en la app, no rellenamos todos los datos obligatorios para que nos vaya mostrando los diferentes mensajes de error, y al final creamos un usuario de cada tipo.

En total son unas cuantas pruebas, 7 en total:
Usuario normal con el nombre vacio -> nombre es obligatorio
Usuario normal con el nombre informado, pero el mail vacío-> mail es obligatorio
Usuario normal con nombre y mail -> usuario creado correctamente
Usuario avanzado con el nombre vacio -> nombre es obligatorio
Usuario avanzado con el nombre informado, pero el mail vacío-> mail es obligatorio
Usuario avanzado con nombre y mail informados, pero numero de empleado vacio -> numero de empleado es obligatorio
Usuario avanzado con nombre, mail y numero de empleado informados -> usuario creado correctamente

En un principio no cuesta demasiado probarlas todas, pero en cuanto las hemos probado un par de veces o tres y añadimos validaciones ( que el mail sea correcto, que el nº de usuario no esté en uso,… ) que hacen que se disparen la cantidad total de pruebas necesarias para cubrir toda la casos, se hace pesado probarlo todo cada vez. Muy pesado. Lo que acaba provocando que no lo probemos todo por cada cambio que hagamos porque «lo que he modificado no afecta» o «estoy seguro de que está bien» pero al final siempre se nos acaba colando un bug y acabamos rompiendo algo.

O podemos hacer un test para cada uno de los casos en el que invocamos el código de crear usuario comparando la respuesta con lo que esperamos que nos devuelva.

assert(crearUsuarioNormal("", "", "")).equal("nombre es obligatorio").
assert(crearUsuarioNormal("Jorge", "", "")).equal("mail es obligatorio").
assert(crearUsuarioNormal("Jorge", "jorge@gmail.com", "")).equal("usuario creado correctamente").
assert(crearUsuarioAvanzado("", "", "")).equal("nombre es obligatorio").
assert(crearUsuarioAvanzado("Jorge", "", "")).equal("mail es obligatorio").
assert(crearUsuarioAvanzado("Jorge", "jorge@mail.com", "")).equal("numero de empleado es obligatorio").
assert( crearUsuarioAvanzado("Jorge", "jorge@mail.com", "3141596")).equal("usuario creado correctamente").

Ahora, cada vez que hagamos un cambio, podemos probar todos los casos y asegurarnos que el resultado es el esperado! 🙂

Pero… hay un pequeño problema…

El código de nuestra aplicación está muy acoplado, por lo que cada vez que pasamos los datos correctamente, se crea un usuario en la base de datos, y tenemos que borrarlo a mano.
Lo que es un coñazo y nos hace volver al punto inicial de que no probemos todo por cada cambio que hagamos porque «lo que he modificado no afecta» y ya sabemos como acaba. Ademas rompe con el primero de los preceptos de los test unitarios, que son los siguientes:

  • Automatizables – no deben requerir operaciones manuales.
  • Completas – deben cubrir la mayor cantidad de código.
  • Repetibles o Reutilizables – deben poder ejecutarse cualquier número de veces.
  • Independientes – la ejecución de un test no debe afectar a la ejecución de otro.
  • Profesionales – deben ser consideradas igual que el código, con la misma profesionalidad, documentación, …

¿Cómo solucionamos esto?

Desacoplando el código que valida la creación del usuario con la base de datos. Ademas aporta otro valor: lo hacemos independiente de la base de datos, si mañana cambia la base de datos (¿alguien ha tenido problemas al migrar a Hana justo por esto?) la lógica de negocio no hay que tocarla, y solo hay que modificar el código que se encarga de trabajar con la base de datos.
Una manera de hacerlo es lo que se llama inyección de dependencias, que se traduce cómo enviar como parámetro la entidad encargada de guardar en base de datos. Si recuerdas los principios SOLID, el 5º es la inversión de dependencias, y la inyección es una de las herramientas para ponerlo en práctica (también se puede hacer mediante un contenedor IOC, por ejemplo).
En nuestro caso vamos a modificar nuestra clase que crea el usuario para que acepte una clase DAO que se encargue de guardar el usuario en base de datos y le pasamos una instancia de ésta como parámetro:
assert(crearUsuarioNormal(usuarioDao, «Jorge», «», «»))
pero esto sigue haciendo que se cree el usuario ¿que ganamos?

Mocks

Si creamos una clase muy similar a usuarioDao, que tenga los mismo métodos pero que no hagan nada, y se la pasamos como parámetro a crearUsuarioNormal, en vez de usuarioDao, podemos hacer nuestro test sin que afecte a la base de datos.
¡Acabamos de descubrir los mocks! ¿Que es eso de un mock? Es un objeto que simula el comportamiento de un objeto real.
Es decir:

  1. tenemos una clase A que hace algo (en nuestro caso guardar un usuario en base de datos)
  2. tenemos una clase B que recibe como parámetro la clase A para utilizarla
  3. creamos una clase Mock con los mismos métodos que la clase A pero con otro comportamiento (en nuestro caso, no hacer nada)
  4. pasamos la clase Mock donde deberíamos pasar la clase original
  5. conseguimos aislar el comportamiento de la clase B, y podemos hacer nuestros tests sin que realmente se cree el usuario

Por tanto nuestra app en pseudo código quedaría algo así:

clase Usuario
  var datos
  var dao
  constructor(usuarioDao) {
    dao = usuarioDao
  }
  metodo validar(nombre, mail, numEmpleado) {
    if(nombre EQUAL "") {
      return "nombre es obligatorio"
    }
    // más validaciones ...
  }
  metodo crearUsuarioNormal(nombre, mail, numEmpleado) {
    var errorValidacion = validar(nombre, mail, numEmpleado)
    if( errorValidacion NOT EQUAL "") {
      return errorValidacion
    }
    dao.crearUsuario(nombre, mail, numEmpleado, false);
  }
  metodo crearUsuarioAvanzado(nombre, mail, numEmpleado) {
    var errorValidacion = validar(nombre, mail, numEmpleado)
    if( errorValidacion NOT EQUAL "") {
      return errorValidacion
    }
    dao. crearUsuario(nombre, mail, numEmpleado, true);
  }
}

clase UsuarioDao
  metodo crearUsuario(nombre, mail, numEmpleado, usuarioAvanzado) {
    //Guardar en base de datos los datos del usuario ...
  }
}

clase MockDao
  metodo crearUsuario(nombre, mail, numEmpleado, usuarioAvanzado) {
    //No hace nada
  }
}

Gracias a nuestro código refactorizado y al mock que hemos creado, ya podemos hacer testing cumpliendo el primer precepto. Haríamos los tests de la siguiente manera:

var mockDao = new MockDao()
var usuario = new Usuario(mockDao)
assert(usuario.crearUsuarioNormal("", "", "")).equal("nombre es obligatorio")

Hemos conseguido hacer una batería de tests que prueban nuestro código de creación de usuarios. Ahora podemos seguir programando y cuando nos cansemos, creamos de nuevo más tests…


¡Pues va a ser que no!

TDD

En este momento entra en juego el principio que marca la diferencia en el uso de test unitarios: Testing Driven Development (o TDD), que se puede traducir como Desarrollo Dirigido por Tests y que significa: primero haz los tests y después el código.
Visto así suena raro ¿cómo voy a testear un código que no existe? Justo ahí está la gracia del asunto.
En el ejemplo que hemos visto, teníamos un código que hemos modificado para poder adecuarlo a los tests. Tener que hacer esto cada vez es pesado, complejo y da pie a tener que reescribir mucho código, por eso escribiendo el test primero nos aseguramos de que ese código va a estar preparado para ser testeado.

Sigamos con el ejemplo, vamos a añadir una nueva funcionalidad a nuestra super app: validar que el mail introducido es correcto, tiene que tener una @ y terminar en .com
1º escribimos un test para probar un caso erróneo: que el mail no tenga @:

var mockDao = new MockDao()
var usuario = new Usuario(mockDao)
assert(usuario.crearUsuarioNormal("Jorge", "jorge_mail", "")).equal("mail incorrecto")

2º Ejecutamos el test, saldrá en rojo.
3º creamos el código que pase este test.

clase Usuario
  metodo validar(nombre, mail, numEmpleado) {
    // validaciones anteriores ...
    if(mail NOT CONTAINS '@') {
      return "mail incorrecto
    }
  }
}

4º volvemos a ejecutar el test ahora saldrá en verde 🙂
Repetimos la secuencia para un nuevo caso erróneo: que el mail no acabe en .com
1º Creamos un nuevo test:

var mockDao = new MockDao()
var usuario = new Usuario(mockDao)
assert(usuario.crearUsuarioNormal("Jorge", "jorge@mail", "")).equal("mail incorrecto")

2º Ejecutamos el test, saldrá en rojo.
3º creamos el código que pase este test.

metodo validar(nombre, mail, numEmpleado) {
  // validaciones anteriores ...
  if(mail NOT CONTAINS '@') {
    return "mail incorrecto
  }
  if(mail NOT ENDS '.com') {
    return "mail incorrecto
  }
}

4º volvemos a ejecutar el test ahora saldrá en verde 🙂 🙂
5º refactorizamos el código: este paso es bastante importante, una vez realizados los pasos anteriores, intentamos mejorar el código

metodo validar(nombre, mail, numEmpleado) {
  // validaciones anteriores ...
  if(mail NOT CONTAINS '@' AND mail NOT ENDS '.com' ) {
    return "mail incorrecto
  }
}

6º volvemos a ejecutar el test ahora saldrá en … ¿¡rojo!? ¡hostia! ¿que ha pasado?
Que me he pasado de listo y al hacer refactor he cometido un error: al poner un AND en la validación, solo sale el mensaje de error si las 2 comparaciones fallan 🙁 pero gracias a los tests, nos hemos dado cuenta. ¿Quien habría vuelto a probar a mano el caso de la @? Yo no.

metodo validar(nombre, mail, numEmpleado) {
  // validaciones anteriores ...
  if(mail NOT CONTAINS '@' OR mail NOT ENDS '.com' ) {
    return "mail incorrecto
  }
}

7º ahora si salen todos los tests en verde 🙂 🙂 🙂

Con ésta demostración del potencial de los test unitarios y TDD doy por concluida este artículo, espero que os haya gustado y lo pongáis en práctica.
Al principio cuesta un poco cambiar la mentalidad, y se hace más lento, pero conforme vas cogiendo soltura escribiendo tests, tu velocidad programando se parece mucho a la que tenias sin tests y ganas mucho tiempo al no tener que hacer tantas pruebas, pero sobre todo, ganas tranquilidad al saber que lo que estás tocando no va a romper nada (nada que no esté cubierto por una buena batería de test, claro).

Y para finalizar, hay otro punto importante que ganas: si te fijas en los descriptivos que les hemos dado a los tests, estas describiendo toda la funcionalidad de una clase. ¿Alguna vez habrías pensado que podías crear documentación sin darte cuenta y que ademas fuera tan útil?

En próximos artículos escribiré como hacer test unitarios en SCP usando javascript y en ABAP.

Categorías: teoría

0 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *