Hace ya un tiempo asistí a una kata TDD en PHPMad, la verdad es que me gustó y pude aprender bastante.
Para entendernos una kata (aplicado a la programación), se traduce en pequeños ejercicios, de menos de 1 hora de duración, que nos ayudan a aprender y mejorar.
Con las katas aparte de aprender podemos mejorar nuestras habilidades y/o nuestros hábitos a la hora de programar. También podemos utilizar atajos de teclado y coger el hábito de «pensar antes de picar». Así que hoy vamos a hacer una pequeña kata.
El problema que he elegido es una calculadora, que aunque sea un ejemplo muy visto, nos irá ilustrando como ir afrontando el reto. El ejemplo está sacado del libro diseño ágil con TDD.
Como base vamos a partir del proyecto https://github.com/matthiasnoback/php-coding-dojo donde tenemos todo lo necesario para hacer funcionar PHPStorm y PHPUnit en conjunto. Si queréis ir echando un vistazo, mis soluciones las iré colgando en mi cuenta de github: https://github.com/jeslopcru/php-coding-dojo
El problema de la calculadora
#Calculator:
Quiero lanzar al mercado un software educativo para enseñar matemáticas a niños. Necesito que puedan jugar o practicar a través de una página web pero también a través del teléfono móvil y quizás más adelante también en la consola Xbox. El juego servirá para que los niños practiquen diferentes temas dentro de las matemáticas y el sistema debe recordar a cada niño, que tendrá unnombre de usuario y una clave de acceso.
El sistema registrará todos los ejercicios que han sido completados y la puntuación obtenida para permitirles subir de nivel si progresan. Existirá unusuario tutor que se registra a la vez que el niño y que tiene la posibilidad de acceder al sistema y ver estadísticas de juego del niño. El tema más importante ahora mismo es la aritmética básica con números enteros. Es el primero que necesito tener listo para ofrecer a los profesores de enseñanza primaria un refuerzo para sus alumnos en el próximo comienzo de curso. El módulo de aritmética base incluye las cuatro operaciones básicas (sumar,restar, multiplicar y dividir) con números enteros. Los alumnos no solo tendrán que resolver los cálculos más elementales sino también resolver expresiones con paréntesis y/o con varias operaciones encadenadas. Así aprenderán la precedencia de los operadores y el trabajo con paréntesis: las propiedades distributiva,asociativa y conmutativa. Los ejercicios estarán creados por el usuario profesor que introducirá las expresiones matemáticas enel sistema para que su resultado sea calculado automáticamente y almacenado. El profesor decide en qué nivel va cada expresiónmatemática. En otros ejercicios se le pedirá al niño que se invente las expresiones matemáticas y les ponga un resultado. El programa dispondrá de una calculadora que sólo será accesible para los profesores y los jugadores de niveles avanzados. La calculadora evaluará y resolverá las mismas expresiones del sistema de juego.
Cuando el jugador consigue un cierto número de puntos puede pasar de nivel, en cuyo caso un email es enviado al tutor para que sea informado de los logros del tutelado. El número mínimo de puntos para pasar de nivel debe ser configurable.
Nuestro primer Test
Lo primero será crear una carpeta para el proyecto llamada CalculatorProject y dentro una carpeta Tests que albergará nuestro primer test.
Como estamos haciendo TDD, debemos tener en mente siempre el ciclo de test driven development Test – Code – Refactor Así crearemos una clase llamada CalculatoerTest con nuestro primer test para la calculadora
<?php
namespace CalculatorProject\Tests;
use CalculatorProject\Calculator;
class CalculatorTest extends \PHPUnit_Framework_TestCase
{
public function testInstance()
{
new Calculator();
}
}
Ahora ejecutamos el test (cmd + R) y obtendremos nuestro primer Rojo
Como sabemos ahora tenemos que escribir el código mínimo para que pase el tests
<?php
namespace CalculatorProject;
class Calculator
{
}
Con esto ya tenemos nuestros tests en verde. Como es nuestro primer test todavía no tenemos que refactorizar, así que hacemos nuestro primer commit. 🙂
¿Cuál es el siguiente paso? Bueno creo que lo mejor será tener cuidar que cuando acabamos de crear la calculadora esté por defecto a 0. Para ello vamos a modificar un poco el test.
public function testResultDefaultsToZero()
{
$calc = new Calculator();
$this->assertSame(0, $calc->getResult());
}
TIP: Cuando queramos hacer una comparación estricta lo mejor es utilizar assertSame en vez de assertEquals
Ahora a por el código:
public function getResult()
{
return 0;
}
Si has visto bien, getResult siempre devuelve 0. Esto es un poco hardcore, pero recordemos que necesitamos el mínimo código para que pase el test. Dentro de nada arreglaremos esto 🙂
A por los tests y nuestro primer refactor
Ahora vamos a hacer un tests para intentar mejorar nuestro código.
public function testResultDefaultsToZero()
{
$calc = new Calculator();
$this->assertSame(0, $calc->getResult());
}
public function testAddNumber()
{
$calc = new Calculator();
$calc->add(7);
$this->assertSame(7, $calc->getResult());
}
Y ahora el código para pasar los test
class Calculator
{
protected $result = 0;
public function add($number)
{
$this->result = $this->result + $number;
}
public function getResult()
{
return $this->result;
}
Ya volvemos a estar en verde y llegados a este punto tenemos que pararnos, hacer un commit y pensar en como refactorizar un poco los tests. Tenemos código repetido que debemos mejorar, así que hallá vamos.
Esta es nuestra clase de test después de refactorizar.
class CalculatorTest extends \PHPUnit_Framework_TestCase
{
protected $calc;
public function setUp()
{
$this->calc = new Calculator();
}
public function testResultDefaultsToZero()
{
$this->assertSame(0, $this->calc->getResult());
}
public function testAddNumber()
{
$this->calc->add(7);
$this->assertSame(7, $this->calc->getResult());
}
}
TIP: Siempre se refactoriza teniendo como base los tests en verde y justo después de hacer un commit por si tenemos que volver atrás.
Haciendo robusta la aplicación
Ya es hora de hacer nuestra calculadora un poco más tolerante a fallos. ¿Qué pasa si pasamos a nuestra calculadora un valor no numérico? En estas situaciones debemos pensar en lanzar excepciones. Como regla más o menos general una frase que podría ayudarnos “Me opongo a esta acción” lanzando esta excepción. Así que continuemos con TDD haciendo un test que nos ayude con las excepciones.
/**
* @expectedException InvalidArgumentException
*/
public function testRequiresNumericValues()
{
$this->calc->add('four');
}
Que resolveremos con este cambio en la función add
public function add($number)
{
if (!is_numeric($number)) {
throw new \InvalidArgumentException;
}
$this->result = $this->result + $number;
}
Ya volvemos a tener nuestros tests de nuevo en verde. El siguiente paso es que podamos pasar un número variable de argumentos, algo como add(1,2,3,4) así que vamos a por nuestro test para continuar con el TDD
public function testAcceptsMultipleArgs()
{
$this->calc->add(2, 4, 3);
$this->assertEquals(9, $this->calc->getResult());
$this->assertNotEquals('Esto es una cadena', $this->getResult());
}
Refactor
Bueno ya lo tenemos completada la suma, así que ahora habrá que ponerse manos a la obra con el resto de operaciones resta, multiplicación y división.
Aquí tenemos el test para la resta:
public function testSubtractNumber()
{
$this->calc->subtract(4);
$this->assertEquals(-4, $this->calc->getResult());
}
Y teniendo el test podemos hacer el código para que pase el test. Si seguimos iterando con TDD, llegaremos a tener una función de resta como esta:
public function subtract()
{
foreach (func_get_args() as $number) {
if (!is_numeric($number)) {
throw new \InvalidArgumentException;
}
$this->result = $this->result - $number;
}
}
¿A qué os suena? Es muy parecida a la función add. Uno de los principios de diseño de software es DRY (Don’t Repeat Yourself) no te repitas, así que el siguiente paso es refactorizar nuestro código manteniendo los tests en verde.
Después de ir poco a poco refactorizando, ya tenemos el nuevo código de producción listo.
class Calculator
{
protected $result = 0;
public function add()
{
$this->calculateAll(func_get_args(), '+');
}
protected function calculateAll(array $numbers, $symbol)
{
foreach ($numbers as $num) {
$this->calculate($num, $symbol);
}
}
protected function calculate($num, $symbol)
{
if (!is_numeric($num)) {
throw new InvalidArgumentException;
}
switch ($symbol) {
case '+':
$this->result += $num;
break;
case '-':
$this->result -= $num;
break;
}
}
public function subtract()
{
$this->calculateAll(func_get_args(), '-');
}
public function getResult()
{
return $this->result;
}
}
Ahora ya añadir los nuevos métodos solo será coser y cantar 🙂
Conclusiones
Hacer katas de código es una buena manera de enfrentarnos a pequeños problemas con los que aprender paso a paso, ir cogiendo buenos hábitos programando como hacer baby step, refactorizar código, mejorar la legibilidad,…
Si te han gustado las katas en mi repositorio de github (https://github.com/jeslopcru/php-coding-dojo) hay unos cuantos ejercicios más. Si te picas siempre puedes echarle un ojo a Soolvet donde encontrarás bastantes ejercicios.