Hace poco empezamos haciendo nuestra primera kata de código utilizando phpunit. En una primera iteración conseguimos una calculadora totalmente funcional. El código de la kata está en github (http://github.com/jeslopcru/php-coding-dojo).
Esta vez vamos a dar un pequeño empujón a la kata, emepzando a utilizar Mockery y sobre todo vamos a seguir aprendiendo buenas prácticas de desarrollo. Para hacer más intenresante la kata podemos activar phpcs o php-cs-fixer para que antes de hacer un commit analicemos el proyecto para hacer que nuestro código se adapte a un estandar. Yo como guía para instalarlo he utilizado esta referencia (http://sergigp.com/phpstorm-integrando-herramientas-de-calidad-de-codigo)
¿Donde lo dejamos?
Teníamos una calculadora funcional, que era capaz de sumar y restar, pero quizás ese switch
es algo “feo”. Para quien no lo recuerde este esta es la función que estamos comentando:
protected function calculate($num, $symbol)
{
if (!is_numeric($num)) {
throw new InvalidArgumentException();
}
switch ($symbol) {
case '+':
$this->result += $num;
break;
case '-':
$this->result -= $num;
break;
}
}
Añadir más funciones a la calculadora haría aumentar el switch y eso creo que no es una buena solución.
Casi cada vez que utilizamos un switch podemos mejorarlo utilizando polimorfismo. Es cierto que esta regla de cambiar switch por polimorfismo no puede usarse siempre, pero sería bueno tenerla siempre en mente.
Vamos a rediseñar un poco, ya que añadir métodos para multiplicar haría que tuviesemos una clase cada vez más grande con un switch cada vez más inmanejable. Por lo que tendremos que rediseñar nuestra calculadora de una manera un poco diferente
Vamos a crear una interfaz con un solo método y crearemos clases para cada operación que implementen esta interfaz.
Esta es la interfaz:
interface Operation
{
/**
* @param integer $num
* @param integer $current
* @return integer
*/
public function run($num, $current);
}
Tener interfaces es una buena práctica de desarrollo, ya que conforman una serie de métodos que las clases deben implementar.
Ahora tenemos que crear las clases que implementan la interfaz, así que vamos a empezar por la suma 🙂
class Addition implements Operation
{
public function run($num, $current)
{
return $current + $num;
}
}
Ahora tendremos que actualizar los tests para utilizar la nueva clase Addition
public function testAddNumber()
{
//Given
$this->calc->setOperands(7);
$this->calc->setOperation(new Addition);
//When
$result = $this->calc->calculate();
//Then
$this->assertSame(7, $result);
}
Esto nos hará volver a tener los tests en rojo, con lo que tendremos que refactorizar para que vuelvan a verde. Lo mejor será ir poco a poco y comentar el resto de tests, ya que como estamos refactorizando habrá inconsistencias entre los tests “old school” y los nuevos test con polimorfismo.
Después de un poco de trabajo así es como queda la clase Calculator
class Calculator
{
protected $result = null;
protected $operands = [];
protected $operation;
public function getResult()
{
return $this->result;
}
public function setOperation(Operation $operation)
{
$this->operation = $operation;
}
public function setOperands($operands)
{
$this->operands = func_get_args();
}
public function calculate()
{
foreach ($this->operands as $number) {
if (!is_numeric($number)) {
throw new InvalidArgumentException();
}
$this->result = $this->operation->run($number, $this->result);
}
return $this->result;
}
}
Sí, quizás esta clase ahora sea un poco más compleja, pero ganamos en extensabilidad, es decir si queremos añadir nueva funcionalidad solo tenemos que crear una clase que implemente la interfaz.
Una vez tenemos esto es hora de ir descomentando los test que teníamos antes para ver si todos funcionan y dejarlo todo en verde 🙂
Así quedarían los tests después de refactorizarlos
class CalculatorTest extends \PHPUnit_Framework_TestCase
{
/** @var Calculator */
protected $calc;
public function setUp()
{
$this->calc = new Calculator();
}
public function testResultDefaultsToNull()
{
$this->assertNull($this->calc->getResult());
}
public function testAddNumber()
{
//Given
$this->calc->setOperands(7);
$this->calc->setOperation(new Addition());
//When
$result = $this->calc->calculate();
//Then
$this->assertSame(7, $result);
}
/**
* @expectedException InvalidArgumentException
*/
public function testRequiresNumericValues()
{
$this->calc->setOperands('four');
$this->calc->setOperation(new Addition());
$this->calc->calculate();
}
public function testAcceptsMultipleArgs()
{
//Given
$this->calc->setOperands(2, 3, 4);
$this->calc->setOperation(new Addition());
//When
$this->calc->calculate();
//Then
$this->assertEquals(9, $this->calc->getResult());
$this->assertNotEquals('Esto es una cadena', $this->getResult());
}
}
Extendiendo la calculadora
Así que vamos a añadir una clase para multiplicar veremos como es muy fácil extender la funcionalidad de la calculadora.
Para extender la calculadora lo primero es crear los test
public function testMultipiesNumbers()
{
//Given
$this->calc->setOperands(2, 2, 3);
$this->calc->setOperation(new Multiplication());
//When
$result = $this->calc->calculate();
//Then
$this->assertSame(12, $result);
}
Una vez que los tests están completados tenemos que crear la funcionalidad mínima para que todo el proyecto vuelva a estar en verde
class Multiplication implements Operation
{
public function run($num, $current)
{
if (is_null($current)) {
return $num;
}
return $current * $num;
}
}
Listo, ya tenemos nuestro código funcionando. Llegados a este punto debemos darnos cuenta de que estamos rompiendo un poco las reglas del juego. No estamos probando las clases de manera aislada, es decir hay acoplamiento entre las clases Calculator y Addition, Multiplication,…
¡Tenemos mucha suerte! sí, ahora podemos aprender a usar Mockery para tener aislados nuestros tests y evitar el acoplamiento. Quizás estos este problema parezca un poco de juguete, pero creo que así es la mejor manera de aprender a utilizar nuevos frameworks y utilidades.
Para instalar Mockery solo tenemos que añadirlo a composer y actualizarlo, la documentación oficial lo explica genial. Tan solo añadir la linea al archivo composer y ejecutar composer update
Empezando a utilizar Mockery
Ya tenemos instalado Mockery, ahora vamos a empezar a utilizarlo para probar nuestras clases en isolación. modificaremos el tests de suma así:
public function testAddNumbers()
{
//Given
$mock = \Mockery::mock('CalculatorProject\Addition');
$mock->shouldReceive('run')
->once()
->with(7, 0)
->andReturn(7);
$this->calc->setOperands(7);
$this->calc->setOperation($mock);
//When
$result = $this->calc->calculate();
//Then
$this->assertSame(7, $result);
}
Y no debemos olvidar crear tests para la clase Addition, ya que si queremos probar nuestras clases de manera separada debemos tener tests para cada clase.
class AdditionTest extends \PHPUnit_Framework_TestCase
{
public function testFindSum()
{
$addition = new Addition();
$result = $addition->run(4, 0);
$this->assertEquals(4, $result);
}
}
En cuanto empezamos a utilizar Mockey vemos que nuestro número de pruebas se reduce dramáticamente. Antes teníamos pruebas para todos los casos (suma, resta, multiplicación,…) y ahora con Mockery tenemos las pruebas separadas y solo probamos justo los métodos de cada clase y no más.
Conclusiones
Como vemos con un poco de esfuerzo tenemos una clase bien probada. Además hemos aprendido a utilizar Mockery y php Code Sniffer.
Con esta kata aparte de un nuevo framework hemos pensado como mejorar nuestro código y como separar las responsabilidades.
Os animo a seguir haciendo katas y seguir mejorando.
¿Has hecho katas? ¿Que opinas de Mockery? ¿Que kata propones?