Hace un par de días tuve la oportunidad de asistir al curso sobre Test Driven Development (Desarrollo orientado a Test o TDD, por sus siglas en inglés) que impartieron Luis Rovirosa y Jordi Anguela a través de codium.team.
Antes del curso desarrollé un pequeño proyecto personal con Ruby on Rails y realizando test Unitarios, así como test a los controladores e incluso algunos test de integración que abarcaban procesos como: el de registro, el de recuperación de la cuenta y el de inicio de sesión .
El curso me ha parecido muy revelador porque, a pesar de tener algunas nociones básicas, haber basado el proyecto anterior en el libro Agile Web Development with Rails 4 y haberme iniciado un poco en el mundo de los test, bastaron unos simples ejercicios para ser consciente de la magnitud del trabajo que implica realizar un buen TDD y no meros test.
Dicho de otro modo: «Los árboles no te dejan ver el bosque.»
En mi caso, me centré en realizar algunos test de varios tipos pero, debido a la falta práctica, los basaba en el diseño y no al revés. De este modo, cambia enfoque lo que dificulta la comprensión del proceso.
El curso tiene una duración de 2 días y está dividido en una parte teórica y una parte práctica organizada en torno a una kata. Durante la parte práctica, ambos instructores tratan de hacerte reflexionar acerca de los problemas planteados.
Las katas se hacen en parejas en la máquina de uno de los dos componentes y realizando rotación de compañeros en cada ejercicio. Este método resulta especialmente enriquecedor, ya que todos los alumnos comparten su enfoque a la hora de solucionar cada problema, utilizando diversos lenguajes de programación que no siempre son conocidos por todos los alumnos, lo que permite abstraerse del lenguaje y centrarse en problema que plantea la kata.
El principal descubrimiento durante las katas es lo lejos que se está a veces de la solución más sencilla para un determinado requisito. En mi opinión, esto ocurre debido a la tendencia generalizada a anticipar el proceso, complicando así más de lo necesario el diseño.
Me resultó especialmente divertida la kata password-validator en la que, a medida que desarrollábamos los distintos test, Luis aparecía de pronto para realizar un transgresor refactor borrando parte de nuestro código (la mayoría de las reglas) y todos los test seguían en verde. No pude evitar sonreír ante la habilidad del profesor.
Por consiguiente todos los test deben tener un determinado objetivo. Los objetivos de los que nos ocupábamos mi compañero y yo no eran de utilidad, es decir, nos encontrábamos desarrollando unos test inútiles en la práctica.
También tuvimos la oportunidad de hacer prácticas en materia de creación de test de seguridad sobre un código legado para más tarde hacer un refactor con cierta seguridad sin modificar el comportamiento observable. Sin duda fue uno de los ejercicios más didácticos del curso.
En otro orden de cosas, ampliaron nuestra perspectiva en materia de realización de test sobre librerías de terceros con herramientas como stub, mock, spy, etc, tanto de manera manual como con librerías especializadas como Prophecy.
En la última kata utilizamos un enfoque diferente donde el diseño Outside-in (Escuela de Londres) en el que se empieza por la funcionalidad final y se implementa poco a poco todo lo necesario.
En conclusión, el curso da pequeñas pinceladas de un amplio proceso de desarrollo de software, que puede servir de base para los más osados que decidan meterse más en harina y especializarse en este ámbito.
El software nos rodea. Está por todas partes, incluso ahora mismo, en esta habitación, puedes verla si miras por la ventana o al encender la televisión. Puedes sentirlo, cuando vas a trabajar, cuando vas al supermercado, cuando pagas tus impuestos. Es el mundo tecnológico que se ha puesto ante tus ojos para ocultarte la verdad: que eres un esclavo del software. Sobre todo del mal software.
jueves, 14 de diciembre de 2017
Curso TDD con codium.team
Etiquetas:
codium.team,
curso tdd,
kata,
tdd
viernes, 8 de diciembre de 2017
TDD - Test Unitarios
Introducción
Cuando desarrollamos software, antes o después (y más bien durante), tendremos que probar nuestro código para verificar si lo estamos implementando de acuerdo a la especificación. De esta forma, una vez que vamos desarrollando nuestra fantástica pieza de software vamos haciendo debugs de la misma hasta cumplir con las especificaciones marcadas. Hasta aquí todo correcto. Tenemos nuestro código probado y funcionando.
¿Qué pasa si tenemos que desarrollar una segunda funcionalidad sobre el código ya desarrollado? No pasaría nada si la nueva funcionalidad no tiene impacto sobre esta pieza inicial, pero en el caso de no ser así, lo cual suele ser habitual, podría ser preciso cambiar algo cambiar un elemento en la implementación actual necesario para el buen funcionamiento de esta nueva funcionalidad en la implementación actual, pero podría no funcionar para la primera funcionalidad que se desarrolló. Con lo que disfrutaríamos del típico “A wild bug has appeared!”.
Como es de esperar, el peor momento para que aparezca ese bug salvaje es producción. ¿Quién no ha disfrutado alguna vez de la oportunidad de tener que resolver un bug en producción? Es fantástico, ¿verdad? Nada como un poco de estrés para mantener vivo el espíritu de lucha (o supervivencia en algún caso).
Por ello, la única manera de garantizar que no se produzca un impacto negativo es volver a probar la primera funcionalidad. Esta estrategia funciona cuando hay pocas funcionalidades, pero a la larga lo más normal es que se abandone la práctica y sólo se pruebe la nueva funcionalidad o a lo sumo alguna funcionalidad crítica.
Afortunadamente hay otra estrategia que consiste en hacer test para cada una de las pruebas y que estos se ejecuten de forma automática cada vez que se desarrolla un nuevo requisito de la especificación.
¿Qué es un test?
Un test es una prueba donde se verifica el correcto funcionamiento de una pieza de código en un entorno controlado. Esta puede ser una función en programación estructural o un objeto en programación orientada a objetos.
Cada test define qué debería hacer la pieza de software que se va a desarrollar. Por ejemplo los datos de entrada que va a tener, el nombre y parámetros de la pieza de código que se va a probar y qué salida debería tener.
¿Cómo hago un test?
Dependiendo del lenguaje de programación existen diversos framewoks para desarrollar los test.
Para PHP está PHPUnit, Ruby tiene Minitest, Python PyUnit, Java Junit, .Net NUnit, entre otros muchos. Cada uno de ellos tiene su correspondiente manera de instalarse.
En el caso de PHP y una vez instalado PHPUnit podremos hacer un test sencillo.
Por ejemplo, se podría tener la siguiente especificación para desarrollar una calculadora sencilla.
Especificación
Se quiere realizar una calculadora simple que tenga los siguientes requisitos.
Requisito: sumar dos números.
La calculadora tiene que poder sumar dos números y devolver el resultado de la suma.
Pues de esta especificación podremos implementar el test más sencillo.
CalculatorTest.php
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
public function testSum(): void
{
$expectedValue = 2;
$parameterA = 1;
$parameterB = 1;
$calculator = new Calculator();
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '1 + 1 should be 2');
}
}
Si ejecutamos la prueba fallará, ya que no hemos creado todavía la clase Calculator ni su método sum para que pase el test. De hecho el primer paso sería crear la clase Calculator, que implemente un método sum con sus dos parámetros el cual devuelve la suma de los mismos.
Sería recomendable que la especificación definiera la casuística lo más detalladamente posible con los casos límite para comprobar que funciona correctamente. En el ejemplo continuaremos simplificándolo y probaremos varios casos fáciles para comprobar que dos números se suman correctamente. De momento evitaremos que los números puedan ser nulos o que no sean enteros naturales.
CalculatorTest.php
final class CalculatorTest extends TestCase
{
public function testSum(): void
{
$calculator = new Calculator();
// Test: 1 + 1 = 2
$expectedValue = 2;
$parameterA = 1;
$parameterB = 1;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '1 + 1 should be 2');
// Test: 2 + 1 = 3
$expectedValue = 3;
$parameterA = 1;
$parameterB = 2;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '2 + 1 should be 3');
// Test: 2 + 2 = 4
$expectedValue = 4;
$parameterA = 2;
$parameterB = 2;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '2 + 2 should be 4');
}
}
Las tres leyes
Según Robert C. Martin en su libro Clean Code se consideran las siguientes leyes:
Primera Ley
No se puede escribir código de producción hasta no haber escrito un test unitario que falle.
No se puede escribir código de producción hasta no haber escrito un test unitario que falle.
Segunda Ley
No se puede escribir más de un test unitario que falle y no compilar también es fallar.
Tercera Ley
No se puede escribir más código de producción que el suficiente para pasar el test que está fallando actualmente.
El ciclo para escribir código de producción pasa por escribir primero un test y acto seguido el código que abarca ese test. De esta manera todo el código de producción estaría cubierto por los test.
Mantener los test limpios
Los test requieren un esfuerzo extra tanto en lo referente a la creación de los mismos como en lo tocante al mantenimiento, ya que son tan importantes o más que el código de producción.
Los test nos aseguran que cualquier cambio que hagamos sobre el código de producción, ya sea un desarrollo nuevo, como una refactorización, y aunque hayamos hecho un nuevo test para el nuevo cambio, el resto del código funciona como debería y no hemos introducido un nuevo bug.
En el caso de tener un juego de test que no esté bien mantenido al final se plantean dos opciones, hacer el esfuerzo de actualizarlo o desecharlo arriesgándonos a no saber cuándo puede surgir un bug debido a un cambio en el código fuente.
Los test requieren un esfuerzo extra tanto en lo referente a la creación de los mismos como en lo tocante al mantenimiento, ya que son tan importantes o más que el código de producción.
Los test nos aseguran que cualquier cambio que hagamos sobre el código de producción, ya sea un desarrollo nuevo, como una refactorización, y aunque hayamos hecho un nuevo test para el nuevo cambio, el resto del código funciona como debería y no hemos introducido un nuevo bug.
En el caso de tener un juego de test que no esté bien mantenido al final se plantean dos opciones, hacer el esfuerzo de actualizarlo o desecharlo arriesgándonos a no saber cuándo puede surgir un bug debido a un cambio en el código fuente.
Cómo mantener los test Limpios
Lo más importante para mantener los test limpios es la legibilidad del código al igual que en producción.
La legibilidad se consigue a partir de la claridad y la simplicidad del código, así como de la densidad de expresión. O dicho de otro modo, en un test se debe tratar de decir mucho con pocas expresiones.
Es recomendable no utilizar las funciones del sistema directamente, sino crear utilidades para utilizar las funciones del sistema. De esta forma se consigue que los test sean más fáciles de escribir y leer.
Es recomendable no utilizar las funciones del sistema directamente, sino crear utilidades para utilizar las funciones del sistema. De esta forma se consigue que los test sean más fáciles de escribir y leer.
Una aserción por test
Es una buena práctica disponer de una aserción por test, ya que facilita la legibilidad del propio test y el lector no tiene que detenerse a entender por qué existen varias aserciones y lo que implica cada una de ellas.
Si continuamos con el ejemplo de la calculadora.
CalculatorTest
final class CalculatorTest extends TestCase
{
public function testSum1And1ShouldReturn2(): void
{
$calculator = new Calculator();
$expectedValue = 2;
$parameterA = 1;
$parameterB = 1;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '1 + 1 should return 2');
}
public function testSum2And1ShouldReturn3(): void
{
$calculator = new Calculator();
$expectedValue = 3;
$parameterA = 1;
$parameterB = 2;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '2 + 1 should return 3');
}
public function testSum2And2ShouldReturn4(): void
{
$calculator = new Calculator();
$expectedValue = 4;
$parameterA = 2;
$parameterB = 2;
$result = $calculator->sum($parameterA, $parameterB);
$this->assertTrue($expectedValue, $result, '2 + 2 should return 4');
}
}
F.I.R.S.T
Según Robert C. Martin en su libro Clean Code los test limpios siguen las siguientes reglas que forman el acrónimo que figura como título de este apartado:
Fast
Los test deberían correr rápido. Por cada cambio en el código deberían ejecutarse todos los test del proyecto. Si los test son lentos no se ejecutarán en cada cambio del código y propiciarán la aparición de bugs en el código.
Independent
Los test deberían ser independientes entre sí y poder ejecutarse en cualquier orden. En caso de que los test dependan unos de otros, podría darse el caso de que un primer test fallar y haciendo fallar el resto en consecuencia, lo haría necesario trabajar más para hallar el problema.
Repetible
Los test deberían ser repetibles en cualquier entorno. Es decir, deberían ser capaces de ejecutarse en cualquier entorno ya sea nuestro propio ordenador, el entorno de desarrollo, test, QA o incluso producción.
Self-Validating
Los test deberían tener sólo dos resultados cuando se ejecutan: “pass” o “fail”. Cualquier otro resultado del test como un log o similar donde haya que buscar manualmente el resultado del test no es una buena práctica.
Timely
Los test necesitan escribirse en el momento oportuno, es decir, antes de escribir el código de producción.
Etiquetas:
clean code,
tdd,
test,
test driven development,
test unitarios
Suscribirse a:
Entradas (Atom)