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.
No hay comentarios:
Publicar un comentario