Apéndice 2: PhpSpec

Phpspec es un framework para BDD (Behavior Driven Design). Se trata de una variante de TDD que se centra en la descripción del comportamiento de los objetos mediante ejemplos.

Principalmente es una herramienta de diseño, y no tanto de testing, aunque es válida para test unitarios, y en cualquier caso no puede usarse para test de integración o de aceptación. Para esos casos utilizaríamos behat, una herramienta de la misma familia.

Instalación

Nos situamos dentro de la carpeta del proyecto, creándola si es necesario:

1 mkdir dojo
2 cd dojo

Dentro del proyecto asumimos la convención de tener las las carpetas src y spec. Esta última se crea automáticamente, por lo que este paso es opcional.

1 mkdir src
2 mkdir spec

Si planeas usar plantillas de código (ver más abajo) debes crear la carpeta .phpspec en la raíz del proyecto.

1 mkdir .phpspec

Si no lo hemos hecho antes, iniciamos el proyecto mediante composer init y como primera dependencia requerimos phpspec.

1 composer init
2 # Fill in with the data needed
3 composer require --dev phpspec/phpspec

Configuración inicial

En cuanto a la configuración, este sería un buen composer.json mínimo para usar phpspec con soporte de PSR-4. También puedes configurar el autoload para que use PSR-0.

 1 {
 2   "name": "talkingbit/dojo",
 3   "description": "A simple space to practice testing",
 4   "minimum-stability": "dev",
 5   "license": "MIT",
 6   "type": "library",
 7   "config": {
 8     "bin-dir": "bin"
 9   },
10   "authors": [
11     {
12       "name": "Fran Iglesias",
13       "email": "franiglesiad@mac.com"
14     }
15   ],
16   "require-dev": {
17     "phpspec/phpspec": "5.0.x-dev"
18   },
19   "autoload": {
20     "psr-4": {
21       "TalkingBit\\Dojo\\": "src/"
22     }
23   },
24   "autoload-dev": {
25     "psr-0": {
26       "": "src/"
27     },
28     "psr-4": {
29       "Tests\\TalkingBit\\Dojo\\": "tests/"
30     }
31   }
32 }

Si basas el autoload en PSR-0 no necesitarías nada más, pero como nosotros lo vamos a configurar para PSR-4, aquí tienes un archivo phpspec.yml de ejemplo (y que podríamos ampliar más adelante para dar soporte a otras características).

1 suites:
2     example_suite:
3         namespace: TalkingBit\Dojo
4         psr4_prefix: TalkingBit\Dojo
5         spec_prefix: spec
6         src_path: '%paths.config%/src'
7         spec_path: '%paths.config%'
  • example_suite: un nombre para la suite de specs que queramos configurar. ** namespace: es la raíz del namespace que hayas definido en composer.json bajo autoload: psr-4.
  • psr4-prefix: en principio, coincide con la anterior, pero se refiere a la ruta que debe usar en el sistema de archivos para guardar el código.
  • spec_prefix: es el nombre de la carpeta que contiene los archivos de especificaciones, que se nombran con el sufijo *Spec.php.
  • src_path: es la ruta bajo la que se guardará el código generado. En el ejemplo %paths.config% apunta a la raíz del proyecto y la carpeta es src.
  • spec_path es la ruta bajo la que se almacenarán las especificaciones, creándose en ella la carpeta definida en spec_prefix.

Para nota: plantillas

phpspec es capaz de ayudarnos con los pasos más tediosos de la generación de código, para lo que utiliza un sistema de plantillas que se pueden personalizar guardando archivos con extensión .tpl en una carpeta .phpspec en la raíz del proyecto. Por ejemplo:

class.tpl

1 <?php
2 declare (strict_types = 1);%namespace_block%
3 
4 class %name%
5 {
6 }

specification.tpl

 1 <?php
 2 
 3 namespace %namespace%;
 4 
 5 use %subject%;
 6 use PhpSpec\ObjectBehavior;
 7 use Prophecy\Argument;
 8 
 9 class %name% extends ObjectBehavior
10 {
11     public function it_is_initializable()
12     {
13         $this->shouldHaveType(%subject_class%::class);
14     }
15 }

Specification by example

En el fondo, las especificaciones mediante ejemplos son equivalentes a los asserts de PHPUnit, pero la forma particular de realizarlas nos ayuda a ver la clase en cuanto a su comportamiento.

Por ejemplo, en PHPUnit escribiríamos un test como este:

1 public function testShouldCalculateThePriceWithDiscount()
2 {
3     $price = new Price(100);
4     $discountedPrice = $price->minusDiscountPercent(15);
5     $this->assertEquals(85, $discountedPrice);
6 }

En phpspec, lo equivalente sería escribir el siguiente ejemplo:

1 public function it_should_calculate_the_price_with_discount()
2 {
3     $this->beConstructedWith(100);
4     $this->$price->minusDiscountPercent(15)->shouldBe(85);
5 }

Veamos las diferencias una a una:

En phpspec

  • Los TestCase se llaman Specification.
  • Los tests se llaman ejemplos y se nombrar comenzando por it_ o its_.
  • En una Specification $this es un proxy a nuestro Subject Under Test.
  • En lugar de assertions usamos matchers, que verifican lo que devuelve el método probado.

Además de estas diferencias que se pueden observar, en phpspec:

  • No se pueden aplicar matchers sobre otra cosa que no sean los métodos del Subject Under Test, dado que $this es un proxy que captura la salida del método original y nos permite testearla.
  • Se pueden definir TestDoubles de forma muy sencilla que son generados mediante el framework Prophecy. Basta indicarlos como parámetros en los ejemplos, tipados con la interfaz o clase que queremos doblar.

Primera especificación

Para ahorrarnos un poco de trabajo phpspec se maneja con dos comandos principales:

  • describe: con el que inicializamos la descripción o spec de una clase.
  • run: con el que ejecutamos los tests.

Describe

El primero es describe y nos permite iniciar la descripción de una clase a través de ejemplos.

Tomando como punto de partida la configuración que acabamos de hacer, vamos a imaginar que queremos describir una clase DojoDomainCustomerCustomer. Lo haríamos así:

1 bin/phpspec describe Dojo/Domain/Customer/Customer

Una forma alternativa que usa la sintaxis de los namespace es:

1 bin/phpspec describe 'Dojo\Domain\Customer\Customer'

Al ejecutar este comando se creará una especificación inicial para esta clase, que se guardará en el archivo spec/Dojo/Domain/Customer/CustomerSpec.php.

El programa devolverá el siguiente resultado:

1 Specification for Dojo\Domain\Customer\Customer created in /Users/frankie/Sites/hlz/\
2 spec/Domain/Customer/CustomerSpec.php.

Y la Spec, creada en la ruta indicada, tendrá esta pinta:

spec/Domain/Customer/CustomerSpec.php

 1 <?php
 2 
 3 namespace spec\Dojo\Domain\Customer;
 4 
 5 use Dojo\Domain\Customer\Customer;
 6 use PhpSpec\ObjectBehavior;
 7 use Prophecy\Argument;
 8 
 9 class CustomerSpec extends ObjectBehavior
10 {
11     function it_is_initializable()
12     {
13         $this->shouldHaveType(Customer::class);
14     }
15 }

Cosas interesantes:

  • La clase CustomerSpec viene a ser el equivalente de un TestSuite de PHPUnit, pero está creado de tal manera que $this es usado como proxy a la clase Customer que es la que estamos especificando. Dicho de otra forma: $this es nuestro SUT (Subject Under Test).
  • El método it_is_initializable es un ejemplo. Equivale a un test. Se escriben en snake_case y deben comenzar por it o its.
  • En el método podemos ver un matcher, un concepto similar a una aserción, y que, en este caso es shouldHaveType (el equivalente assertInstanceOf).

Run

Una vez que hemos escrito nuestra primera Spec, el siguiente paso es ejecutarla, como corresponde a una metodología TDD. Evidentemente, como nos exige el ciclo Red-Green-Refactor de TDD, no hemos creado todavía la clase Customer, pero eso llegará en su momento.

1 bin/phpspec run

El comando anterior ejecutará todas las Spec que pueda encontrar. Si queremos ser más precisos podemos indicarlo de varias maneras.

Por ejemplo, usando el namespace (las comillas son obligatorias):

1 bin/phpspec run 'Dojo\Domain\Customer\Customer'

O bien, la ruta completa al archivo de la Spec:

1 bin/phpspec run spec/Dojo/Domain/Customer/CustomerSpec.php

O bien, indicando una ruta en la que esté incluida nuestra Spec:

1 bin/phpspec run spec/Dojo/Domain/

En cualquier caso, el test fallará y nos devolverá el siguiente resultado (los detalles pueden cambiar un poco dependiendo de la forma de invocarla):

 1 Dojo/Domain/Customer/Customer                                                   
 2   11  - it is initializable
 3       class Dojo\Domain\Customer\Customer does not exist.
 4 
 5                                       100%                                       1
 6 1 specs
 7 1 example (1 broken)
 8 6ms
 9 
10                                                                                 
11   Do you want me to create `Dojo\Domain\Customer\Customer` for you?             
12                                                                          [Y/n] 

Fíjate que al final se ofrece a crear la clase descrita por ti, para lo cual bastará con responder y.

1 Class Dojo\Domain\Customer\Customer created in /Users/frankie/Sites/hlz/src/Domain/C\
2 ustomer/Customer.php.
3 
4                                       100%                                       1
5 1 specs
6 1 example (1 passed)
7 7ms

¿Qué ha pasado aquí?

Pues que phpspec ha creado la clase descrita en el lugar adecuado y ha vuelto a ejecutar la Spec, que ahora está en verde (en este artículo no está coloreada la salida).

La clase ha sido creada así:

spec/Domain/Customer/CustomerSpec.php

1 <?php
2 declare (strict_types = 1);
3 
4 namespace Dojo\Domain\Customer;
5 
6 class Customer
7 {
8 }

En general, cada vez que phpspec se encuentre con algo que puede ayudarnos a crear nos ofrecerá la opción. Como veremos más adelante, eso incluye los nuevos métodos que podamos añadir a nuestra clase, así como Interfaces de colaboradores. Pero ahora no adelantemos acontecimientos.

A partir de ahora, nuestra tarea será ir creando nuevos ejemplos de test que fallen, ejecutarlos y, cuando fallen, implementar el código mínimo necesario para pasar.