Principio FIRST

2.428 palabras · 10 minutos

22/12/2010

El texto que vas a poder leer a continuación explica en qué consisten las cualidades que forman parte del principio FIRST, las cuales tienen como objetivo mejorar la calidad de los tests que prueben nuestro software.

Te encuentras ante un documento de carácter técnico que no entra en demasiado detalle para conseguir que cualquier persona pueda leerlo, aunque inevitablemente aparecerán algunos términos y vocablos propios de la jerga informática que se explicarán de una manera sencilla según aparezcan.

¿Qué cualidades deben tener nuestros tests?

Si estás leyendo esto seguramente estés familiarizado con conceptos tales como TDD, integración continua, tests unitarios, tests de integración, tests de estrés, test funcionales, etc. Sino, no te preocupes, daré una explicación rápida para que más adelante puedas entender fácilmente por qué es tan importante que los tests que desarrollemos cumplan con las cinco cualidades del principio FIRST.

Una parte implícita al desarrollo del software es la de la realización de pruebas que garanticen su correcto funcionamiento. Desarrollar software únicamente no basta, hay que probar que este software funciona y que además funciona correctamente. Para ello o bien realizamos pruebas de manera manual, utilizando nosotros directamente el software, o bien programamos tests automáticos que prueben que el software funciona correcta o incorrectamente por nosotros.

Hay varios tipos de tests. Si lo que queremos probar es que un sistema se comunique con otro crearemos tests de integración. Si queremos probar cuántas peticiones es capaz de soportar antes de encontrarse con problemas, estaremos creando tests de estrés. Si queremos probar el funcionamiento de una unidad de código, diremos que estamos creando un test unitario.

Hay técnicas que centran el desarrollo de nuestras aplicaciones en torno al cumplimiento de tests, de tal manera que lo primero que definiremos es qué queremos que haga el software, programaremos unos tests automáticos en base a ello y, finalmente, desarrollaremos nuestro software con el objetivo de conseguir pasar esos tests. A esta manera de trabajar se la conoce con el nombre de TDD (Test Driven Development, o desarrollo dirigido por tests)

En las páginas siguientes se presentarán las cualidades que, según Robert Martin en su libro Clean Code, todo buen test debería cumplir. Estas cualidades se presentan mediante el acrónimo FIRST, cuyo significado es:

Primera cualidad: Fast (rápido)

Una de las principales incomodidades con la que nos encontramos los desarrolladores a la hora de construir software aparece cuando hay que realizar pruebas de manera manual, hasta tal punto que éstas, ya sea por dejadez, por falta de disciplina, por tedio o porque se acaban los plazos de entrega no se realizan con el celo profesional que les corresponde.

Realizar pruebas de manera manual tiene muchas "virtudes", entre las cuales se encuentra la pérdida de tiempo que supone realizarlas ya que, por lo general y por cada prueba:

Tras seguir estos pasos podremos determinar si efectivamente los cambios que hemos hecho funcionan como esperamos o no. Muchas veces hay que montar todo esto para probar algo tan sencillo como que el total de una factura coincide con la suma de los conceptos que la forman.

Como veis, una comprobación tan simple como que una suma se realiza correctamente supone gastar más tiempo del que se debería. Es importante probar que la suma es correcta, pero es deseable tardar lo menos posible en realizar dicha comprobación.

En cualquier caso no sólo hablamos del tiempo que se gasta: enfrenarnos siempre a la misma pantalla de login, acceder con el mismo usuario y contraseña, poner los mismos datos, acceder a las mismas pantallas, una y otra vez, un día tras otro, semana tras semana es bastante poco productivo.

Si conseguimos automatizar las pruebas que tenemos que realizar habremos ganado mucho terreno, pero esto no basta. Una cualidad básica y fundamental que tienen que tener los tests es la de que deben ejecutarse rápidamente, haciéndonos perder el menor tiempo posible.

Nos estamos ahorrando el tener que arrancar aplicaciones, el acceder a la aplicación, el navegar por las opciones, el introducir cantidades a mano, el comprobar visualmente los resultados, etc. Sin duda, estamos ganando tiempo y, sobre todo, aumentando nuestra productividad.

Ejecutar los tests nos ayuda a detectar los errores cuanto antes y a solucionarlos antes de que sea tarde. Si los tests tardan poco en hacer su labor no nos importará ejecutarlos las veces que haga falta, pero si tardan mucho intentaremos utilizarlos con menos frecuencia con lo que perderán efectividad, de ahí que su rapidez sea un factor importante.

Cuando estemos desarrollando una funcionalidad como, por ejemplo, sacar dinero de un cajero del banco, necesitaremos comprobar rápidamente que un cliente no puede sacar más dinero que el que tiene, o más dinero que el límite que tenga por día. Necesitamos que los tests unitarios sean rápidos, y que esas comprobaciones no tarden más que unos pocos segundos para lanzarlos cuantas más veces, mejor.

Pero claro, tipos de tests hay muchos. Aparte de los tests unitarios hay otros como tests de integración, funcionales, de estrés, etc. Estos tests pueden tardar de unos pocos a muchos minutos en ejecutarse, con lo cual quizá lo mejor sea planificarlos o delegar en otro sistema que se encargue de lanzarlos cuando sea oportuno.

Segunda cualidad: Independent (independiente)

Para facilitarnos la tarea de detección de errores es muy importante que los tests sean independientes los unos de los otros. Para lograr esto deberemos evitar a toda costa que las salidas unos tests se utilicen como entradas de otros. Es más, no deberíamos preocuparnos del orden en el cual se vayan a ejecutar los tests ya que cada ejecución puede tener de hecho una ordenación distinta.

Si cada vez que ejecutamos los tests obtenemos resultados diferentes, como por ejemplo que unas veces fallen unos tests y otras veces otros, ¿cómo podremos hacer para saber qué es lo que realmente está fallando?.

Pongamos un ejemplo. Supongamos que estamos probando el comportamiento de un puesto de churros, en el cual siempre hay:

Partamos de la base de que a la hora de ejecutar los tests disponemos de 100 churros para vender, tenemos 20 preparándose y hay un cliente que quiere comprar 12. En este escenario podríamos probar dos cosas diferentes:

Si no diseñamos los tests para que estos sean independientes ocurrirá lo siguiente:

Una primera ejecución de los tests empieza con 100 churros.

Una segunda ejecución de los tests empieza con 100 churros.

Si os fijáis estamos creando una dependencia artificial entre las pruebas, ya que ambas utilizan una cantidad común de churros para hacer sus cálculos. El problema viene dado porque ambas pruebas no son independientes entre sí: si una añade o quita churros está perjudicando a la otra.

La solución pasaría por, o bien dejar de nuevo la cantidad de churros listos a 100 al terminar la ejecución, o bien no utilizar esa cantidad común para hacer los cálculos y crear test independientes.

En cada una de las dos ejecuciones hemos visto que había tests que fallaban, y lo peor es que cada vez ha fallado algo distinto. En este caso todo ha sido muy sencillo porque sólo hay dos tests y el problema es fácil de detectar, pero podríamos haber tenido una batería con cientos de tests y encontrarnos con un auténtico problema.

Tercera cualidad: Repeteable (repetible)

¿A quién no le ha pasado alguna vez que ha visto que algo no funciona en su ordenador y sí en el de otra persona?. Cuando desarrollamos software, ¿cuántas veces habréis podido ver que una característica funciona única y exclusivamente en el ordenador del desarrollador que la ha creado?

Esto es un problema mayor del que a priori pudiera parecer. Si todo tu desarrollo funciona únicamente en un ordenador te estás poniendo la zancadilla tú mismo al limitar el desarrollo de tu propio software. Si ese ordenador se estropease o algo cambiase, ¿qué harías?

Con la ejecución de los tests de prueba ocurre algo parecido. No podemos permitir que las pruebas funcionen en unos ordenadores si y en otros no por una razón muy sencilla: todos los miembros del equipo de desarrollo deben poder probar que cualquier cambio que hayan hecho no haya afectado al software que están construyendo entre todos.

De este modo, si los tests pasan para un desarrollador deben pasar para todos, y al revés, si algún test falla, por pequeño que sea, debe fallarle a todos los desarrolladores por igual. Los resultados deben ser repetibles en distintos ordenadores.

Ahora bien, ¿qué puede propiciar el que haya esta disparidad?. Muchas veces son cosas sencillas, como por ejemplo:

Una solución a este problema y a otros es contar con un servidor de integración continua que actúe como referente. Dicho servidor deberá estar instalado en un ordenador que no estén utilizando los desarrolladores para programar.

Cada vez que un desarrollador termina una funcionalidad y la deja a disposición de todos, el servidor de integración continua generará el software al completo y lanzará los tests sobre él. Los resultados de ese servidor deben ser la referencia para todos: si los tests pasan allí deberían pasar en todos los ordenadores de los desarrolladores, y si falla algún test, ese mismo test debería fallar en todos los ordenadores de los desarrolladores. En caso de no ser así, es importante determinar la causa que provoca esto y solucionarlo cuanto antes.

Cuarta cualidad: Self-validating (auto evaluable)

Sin duda, una de las mayores ventajas que tienen los test automáticos es que trabajan por nosotros. Nos ahorran hacer tareas repetitivas, tiempo, esfuerzo, y gracias a ellos es muy sencillo comprobar que al cambiar algo en la actualidad no estamos estropeando algo que funcionaba en el pasado.

Pero, aparte de todo esto, ¿podrían hacer algo más por nosotros?. Sí, podrían ayudarnos a saber si hemos obtenido los resultados que esperábamos o no siendo auto evaluables.

A nosotros lo que nos interesa es poder lanzar los tests y que ellos mismos sean capaces de determinar si todo ha ido bien o si, por el contrario, algo ha ido mal, de tal modo que nosotros con un simple vistazo podamos saber cuál ha sido el resultado de la ejecución de los tests.

Para conseguir esta meta es necesario que los desarrolladores no tengamos que ver la traza que ha seguido el test para ejecutarse ni saber de forma pormenorizada qué ha hecho para alcanzar el objetivo final ni cuál es, a no ser que nos encontremos problemas, en cuyo caso siempre será interesante saber qué ha fallado y por qué.

Si nuestro test, por ejemplo, calcula la inversa de matrices cuadradas, ¿qué será mejor?

El segundo caso es mejor, ¿verdad?. Nos ahorra tiempo y nos facilita la obtención de resultados. Es por ello que deberemos conseguir que todos nuestros tests tengan la capacidad de autoevaluarse.

Quinta cualidad: Timely (oportuno)

En el mundo del desarrollo de pruebas de software una pregunta mil veces formulada es la de "¿Cuándo debo definir los tests?", cuya respuesta es "Antes de ponerte a implementar la funcionalidad que necesites".

El motivo es el muy sencillo: es más fácil hacer tests para un código que todavía no está escrito que para uno que ya ha sido creado, del mismo modo que es más fácil hacer crecer recto un árbol que todavía no ha brotado con una guía que enderezar uno que tiene varios metros de altura.

Otra de las ventajas de crear test en el momento oportuno es que si programas con la mentalidad de que tienes que hacer tests que probarán la funcionalidad harás código que sea fácilmente testeable.

Otro punto que juega a favor de definir los tests con antelación es la de que te permitirán saber cuándo has terminado la implementación de la funcionalidad, que será cuando tu código pase todos los tests. Un efecto colateral es que se evita desviar el desarrollo a la implementación de funcionalidades que no es necesario implementar, pudiendo centrarnos únicamente en solucionar el problema concreto que queremos resolver.