Advertisement
Scroll to top
Read Time: 17 min

() translation by (you can also view the original English article)

En este tutorial corto pero completo, veremos el desarrollo impulsado por el comportamiento (BDD) con phpspec. Principalmente, será una introducción a la herramienta phpspec, pero a medida que avancemos, abordaremos diferentes conceptos de BDD. BDD es un tema candente en estos días y phpspec ha ganado mucha atención en la comunidad PHP recientemente.

SpecBDD y Phpspec

BDD se trata de describir el comportamiento del software para lograr el diseño correcto. A menudo se asocia con TDD, pero mientras que TDD se centra en probar tu aplicación, BDD se trata más de describir su comportamiento. El uso de un enfoque BDD te obligará a considerar constantemente los requisitos reales y el comportamiento deseado del software que estás construyendo.

Dos herramientas BDD han ganado mucha atención en la comunidad PHP recientemente, Behat y phpspec. Behat te ayuda a describir el comportamiento externo de tu aplicación, utilizando el lenguaje Gherkin legible. phpspec, por otro lado, te ayuda a describir el comportamiento interno de tu aplicación, escribiendo pequeñas "especificaciones" en el lenguaje PHP - de ahí SpecBDD. Estas especificaciones están probando que tu código tenga el comportamiento deseado.

Lo que haremos

En este tutorial, cubriremos todo lo relacionado con empezar a usar phpspec. En nuestro camino, crearemos las bases de una aplicación de lista de tareas, paso a paso, utilizando un enfoque SpecBDD. ¡A medida que avanzamos, tendremos a phpspec liderando el camino!

Nota: Este es un artículo intermedio sobre PHP. Asumo que tienes una buena comprensión de PHP orientado a objetos.

Instalación

Para este tutorial, asumo que tienes las siguientes cosas en funcionamiento:

  • Una configuración PHP en funcionamiento (mínimamente la versión 5.3)
  • Composer

Instalar phpspec a través de Composer es la forma más sencilla. Todo lo que tienes que hacer es ejecutar el siguiente comando en una terminal:

1
$ composer require phpspec/phpspec
2
Please provide a version constraint for the phpspec/phpspec requirement: 2.0.*@dev

Esto creará un archivo composer.json para ti e instalará phpspec en un directorio vendor/.

Para asegurarte de que todo esté funcionando, ejecuta phpspec y verifica que obtienes el siguiente resultado:

1
$ vendor/bin/phpspec run
2
3
0 specs
4
0 examples 
5
0ms

Configuración

Antes de comenzar, necesitamos hacer unas configuraciones. Cuando se ejecuta phpspec, busca un archivo YAML llamado phpspec.yml. Puesto que vamos a poner nuestro código en un espacio de nombres, tenemos que asegurarnos de que phpspec sabe acerca de esto. Además, mientras estamos en ello, vamos a asegurarnos de que nuestras especificaciones se vean bien y bonitas cuando las ejecutamos.

Continúa y haz el archivo con el siguiente contenido:

1
formatter.name: pretty
2
suites:
3
    todo_suite:
4
        namespace: Petersuhm\Todo

Hay muchas otras opciones de configuración disponibles, sobre las que puedes leer en la documentación.

Otra cosa que debemos hacer es decirle a Composer cómo cargar automáticamente nuestro código. phpspec utilizará el autocargador de Composer, por lo que es necesario para que se ejecuten nuestras especificaciones.

Agrega un elemento de carga automática al archivo composer.json que Composer creó para ti:

1
{
2
    "require": {
3
        "phpspec/phpspec": "2.0.*@dev"
4
    },
5
    "autoload": {
6
        "psr-0": {
7
            "Petersuhm\\Todo": "src"
8
        }
9
    }
10
}

La ejecución de composer dump-autoload actualizará el cargador automático después de este cambio.

Nuestra primera especificación

Ahora estamos listos para escribir nuestra primera especificación. Comenzaremos describiendo una clase llamada TaskCollection. Haremos que phpspec genere una clase de especificación para nosotros usando el comando describe (o alternativamente la versión corta desc).

1
$ vendor/bin/phpspec describe "Petersuhm\Todo\TaskCollection"
2
$ vendor/bin/phpspec run
3
Do you want me to create `Petersuhm\Todo\TaskCollection` for you? y

Entonces, ¿qué pasó aquí? Primero, le pedimos a phpspec que creara una especificación para TaskCollection. En segundo lugar, ejecutamos nuestro conjunto de especificaciones y luego phpspec se ofreció automáticamente para crear la clase actual TaskCollection para nosotros. Genial, ¿no?

Continúa, ejecuta el conjunto de nuevo, y verás que ya tenemos un ejemplo en nuestras especificaciones (veremos en un momento lo que es un ejemplo):

1
$ vendor/bin/phpspec run
2
3
      Petersuhm\Todo\TaskCollection
4
5
  10  ✔ is initializable
6
7
8
1 specs
9
1 examples (1 passed)
10
7ms

A partir de esta salida, podemos ver que TaskCollection es inicializable. ¿De qué se trata esto? Echa un vistazo al archivo de especificaciones que generó phpspec, y debería ser más claro:

1
<?php
2
3
namespace spec\Petersuhm\Todo;
4
5
use PhpSpec\ObjectBehavior;
6
use Prophecy\Argument;
7
8
class TaskCollectionSpec extends ObjectBehavior
9
{
10
    function it_is_initializable()
11
    {
12
        $this->shouldHaveType('Petersuhm\Todo\TaskCollection');
13
    }
14
}

La frase 'es inicializable' se deriva de una función llamada it_is_initializable() que phpspec ha agregado a una clase llamada TaskCollectionSpec. Esta función es a lo que nos referimos como un ejemplo. En este ejemplo en particular, tenemos lo que llamamos un comparador llamado shouldHaveType() que verifica el tipo de nuestra clase TaskCollection. Si cambias el parámetro pasado a esta función por otra cosa y ejecutas la especificación nuevamente, verás que fallará. Antes de comprender completamente esto, creo que debemos investigar a qué se refiere la variable $this en nuestra especificación.

¿Qué es $this?

Por supuesto, $this se refiere a la instancia de la clase TaskCollectionSpec, ya que esto es sólo código PHP normal. Pero con phpspec, tienes que tratar a $this diferente de lo que normalmente haces, ya que bajo el capó, en realidad se refiere al objeto bajo prueba, que es de hecho la clase TaskCollection. Este comportamiento se hereda de la clase ObjectBehavior, que se asegura de que las llamadas a funciones sean mediante proxy a la clase de especificación. Esto significa que SomeClassSpec hará llamadas de método proxy a una instancia de SomeClass. phpspec ajustará estas llamadas a métodos para ejecutar sus valores de retorno en comparadores como el que acabas de ver.

No necesitas una comprensión profunda de esto con el fin de utilizar phpspec, sólo recuerda que, en lo que a ti respecta, $this en realidad se refiere al objeto bajo prueba.

Construyendo nuestra colección de tareas

Hasta ahora, no hemos hecho nada nosotros mismos. Pero phpspec ha creado una clase TaskCollection vacía para que la usemos. Ahora es el momento de completar un código y hacer que esta clase sea útil. Agregaremos dos métodos: un método add(), para agregar tareas, y un método count(), para contar el número de tareas en la colección.

Agregando una tarea

Antes de escribir cualquier código real, debemos escribir un ejemplo en nuestra especificación. En nuestro ejemplo, queremos intentar agregar una tarea a la colección y luego asegurarnos de que la tarea se haya agregado. Para hacer esto, necesitamos una instancia de la clase Task (hasta ahora inexistente). Si agregamos esta dependencia como parámetro a nuestra función de especificación, phpspec nos dará automáticamente una instancia que podemos usar. En realidad, la instancia no es una instancia real, sino a lo que phpspec se refiere como Collaborator. Este objeto actuará como el objeto real, pero phpspec nos permite hacer cosas más sofisticadas con esto, que veremos pronto. Aunque la clase Task no existe todavía, por ahora, solo finge que sí. Abre TaskCollectionSpec y agrega una declaración de use para la clase Task y luego agrega el ejemplo it_adds_a_task_to_the_collection():

1
use Petersuhm\Todo\Task;
2
3
...
4
5
function it_adds_a_task_to_the_collection(Task $task)
6
{
7
    $this->add($task);
8
    $this->tasks[0]->shouldBe($task);
9
}

En nuestro ejemplo, escribimos el código "desearíamos tener". Llamamos al método add() y luego intentamos asignarle una $task. Luego verificamos que la tarea se haya agregado a la variable de instancia $tasks. El comparador shouldBe() es un comparador de identidad similar al comparador === de PHP. Puedes usar shouldBe(), shouldBeEqualTo(), shouldEqual() o shouldReturn(); todos hacen lo mismo.

La ejecución de phpspec producirá algunos errores, ya que todavía no tenemos una clase llamada Task.

Hagamos que phpspec arregle eso para nosotros:

1
$ vendor/bin/phpspec describe "Petersuhm\Todo\Task"
2
$ vendor/bin/phpspec run
3
Do you want me to create `Petersuhm\Todo\Task` for you? y

Ejecutando phpspec de nuevo, algo interesante sucede:

1
$ vendor/bin/phpspec run
2
Do you want me to create `Petersuhm\Todo\TaskCollection::add()` for you? y

¡Perfecto! Si echas un vistazo al archivo TaskCollection.php, verás que phpspec hizo una función add() para que la llenemos:

1
<?php
2
3
namespace Petersuhm\Todo;
4
5
class TaskCollection
6
{
7
8
    public function add($argument1)
9
    {
10
        // TODO: write logic here

11
    }
12
}

phpspec todavía se queja, sin embargo. No tenemos una matriz $tasks, así que vamos a hacer una y le vamos a agregar la tarea:

1
<?php
2
3
namespace Petersuhm\Todo;
4
5
class TaskCollection
6
{
7
    public $tasks;
8
9
    public function add(Task $task)
10
    {
11
        $this->tasks[] = $task;
12
    }
13
}

Ahora nuestras especificaciones son todas agradables y verdes. Ten en cuenta que me aseguré de escribir el parámetro $task.

Sólo para asegurarnos de que lo hicimos bien, vamos a agregar otra tarea:

1
function it_adds_a_task_to_the_collection(Task $task, Task $anotherTask)
2
{
3
    $this->add($task);
4
    $this->tasks[0]->shouldBe($task);
5
6
    $this->add($anotherTask);
7
    $this->tasks[1]->shouldBe($anotherTask);
8
}

Ejecutando phpspec, parece que todos estamos bien.

Implementando la la interfaz Countable

Queremos saber cuántas tareas hay en una colección, lo que es una gran razón para usar una de las interfaces de la Biblioteca PHP Estándar (SPL), a saber, la interfaz Countable. Esta interfaz dicta que una clase que la implementa debe tener un método count().

Anteriormente, usamos el comparador shouldHaveType(), que es un comparador de tipos. Utiliza el comparador de PHP instanceof para validar que un objeto es de hecho una instancia de una clase determinada. Hay 4 comparadores de tipos, que hacen lo mismo. Uno de ellos es shouldImplement(), que es perfecto para nuestro propósito, así que continuemos y usemos eso en un ejemplo:

1
function it_is_countable()
2
{
3
    $this->shouldImplement('Countable');
4
}

¿Ves lo hermoso que se lee? Vamos a ejecutar el ejemplo y hacer que phpspec nos guíe:

1
$ vendor/bin/phpspec run
2
3
        Petersuhm/Todo/TaskCollection
4
  25  ✘ is countable
5
        expected an instance of Countable, but got [obj:Petersuhm\Todo\TaskCollection].

Bien, nuestra clase no es una instancia de Countable ya que aún no lo hemos implementado. Vamos a actualizar el código de nuestra clase TaskCollection:

1
class TaskCollection implements \Countable

Nuestras pruebas no se ejecutarán, ya que la interfaz Countable tiene un método abstracto, count(), que tenemos que implementar. Un método vacío hará el truco por ahora:

1
public function count()
2
{
3
    // ...

4
}

Y volvemos al verde. Por el momento, nuestro método count() no hace mucho y en realidad es bastante inútil. Escribamos una especificación para el comportamiento que deseamos que tenga. Primero, sin tareas, se espera que nuestra función de conteo devuelva cero:

1
function it_counts_elements_of_the_collection()
2
{
3
    $this->count()->shouldReturn(0);
4
}

Devuelve null, no 0. Para obtener una prueba verde, vamos a solucionar esto de la manera TDD/BDD:

1
public function count()
2
{
3
    return 0;
4
}

Estamos en verde y todo está bien, pero este probablemente no es el comportamiento que queremos. En su lugar, vamos a expandir nuestras especificaciones y a agregar algo a la matriz $tasks:

1
function it_counts_elements_of_the_collection()
2
{
3
    $this->count()->shouldReturn(0);
4
5
    $this->tasks = ['foo'];
6
    $this->count()->shouldReturn(1);
7
}

Por supuesto, nuestro código sigue devolviendo 0, y tenemos un paso en rojo. Corregir esto no es demasiado difícil y nuestra clase TaskCollection ahora debería tener este aspecto:

1
<?php
2
3
namespace Petersuhm\Todo;
4
5
class TaskCollection implements \Countable
6
{
7
    public $tasks;
8
9
    public function add(Task $task)
10
    {
11
        $this->tasks[] = $task;
12
    }
13
14
    public function count()
15
    {
16
        return count($this->tasks);
17
    }
18
}

Tenemos una prueba verde y nuestro método count() funciona. ¡Qué gran día!

Expectativas y promesas

¿Recuerdas que te dije que phpspec te permite hacer cosas geniales con instancias de la clase Collaborator, AKA las instancias que son inyectadas automáticamente por phpspec? Si has estado escribiendo pruebas unitarias antes, sabes qué son los simulacros y los sustitutos. Si no lo haces, por favor no te preocupes demasiado por eso. Es sólo jerga. Estas cosas se refieren a objetos "falsos" que actuarán como tus objetos reales, pero que te permitirán realizar pruebas de forma aislada. phpspec convertirá automáticamente estas instancias de Collaborator en simulacros y sustitutos si lo necesitas en tus especificaciones.

Esto es realmente impresionante. Bajo el capó, phpspec utiliza la biblioteca Prophecy, que es un framework para pruebas muy opinado que juega bien con phpspec (y es desarrollado por la misma gente impresionante). Puedes establecer una expectativa en un colaborador (mocking), como "este método debe llamarse", y puedes agregar promesas (stubbing), como "este método devolverá este valor". Con phpspec esto es realmente fácil y haremos ambas cosas a continuación.

Vamos a hacer una clase, la llamaremos TodoList, que puede hacer uso de nuestra colección de la clase.

1
$ vendor/bin/phpspec desc "Petersuhm\Todo\TodoList"
2
$ vendor/bin/phpspec run
3
Do you want me to create `Petersuhm\Todo\TodoList` for you? y

Agregando tareas

El primer ejemplo que agregaremos es uno para agregar tareas. Haremos un método addTask(), que no hace nada más que agregar una tarea a nuestra colección. Simplemente dirige la llamada al método add() en la colección, por lo que este es un lugar perfecto para hacer uso de una expectativa. No queremos que el método realmente llame al método add(), solo queremos asegurarnos de que intenta hacerlo. Además, queremos asegurarnos de que lo llame solo una vez. Echa un vistazo a cómo podemos hacer esto con phpspec:

1
<?php
2
3
namespace spec\Petersuhm\Todo;
4
5
use PhpSpec\ObjectBehavior;
6
use Prophecy\Argument;
7
use Petersuhm\Todo\TaskCollection;
8
use Petersuhm\Todo\Task;
9
10
class TodoListSpec extends ObjectBehavior
11
{
12
    function it_is_initializable()
13
    {
14
        $this->shouldHaveType('Petersuhm\Todo\TodoList');
15
    }
16
17
    function it_adds_a_task_to_the_list(TaskCollection $tasks, Task $task)
18
    {
19
        $tasks->add($task)->shouldBeCalledTimes(1);
20
        $this->tasks = $tasks;
21
22
        $this->addTask($task);
23
    }
24
}

En primer lugar, tenemos a phpspec para proporcionarnos los dos colaboradores que necesitamos: una colección de tareas y una tarea. Luego establecemos una expectativa en el colaborador de la colección de tareas que básicamente dice: "el método add() debe llamarse exactamente 1 vez con la variable $task como parámetro". Así es como preparamos a nuestro colaborador, que ahora es un simulacro, antes de asignarlo a la propiedad $tasks en el TodoList. Por último, intentamos llamar realmente al método addTask().

Bien, ¿qué tiene que decir phpspec sobre esto?:

1
$ vendor/bin/phpspec run
2
3
        Petersuhm/Todo/TodoList
4
  17  ! adds a task to the list
5
        property tasks not found.

La propiedad $tasks es inexistente - fácil:

1
<?php
2
3
namespace Petersuhm\Todo;
4
5
class TodoList
6
{
7
    public $tasks;
8
}

Inténtalo de nuevo, y haz que phpspec guíe nuestro camino:

1
$ vendor/bin/phpspec run
2
Do you want me to create `Petersuhm\Todo\TodoList::addTask()` for you? y
3
$ vendor/bin/phpspec run
4
5
        Petersuhm/Todo/TodoList
6
  17  ✘ adds a task to the list
7
        some predictions failed:
8
          Double\Petersuhm\Todo\TaskCollection\P4:
9
            Expected exactly 1 calls that match:
10
              Double\Petersuhm\Todo\TaskCollection\P4->add(exact(Double\Petersuhm\Todo\Task\P3:000000002544d76d0000000059fcae53))
11
            but none were made.

Bien, ahora sucedió algo interesante. ¿Ves el mensaje "Se esperaba exactamente 1 llamadas que coinciden: ..."? Ésta es nuestra expectativa fallida. Esto sucede porque después de llamar al método addTask(), no se llamó al método add() de la colección, como esperábamos.

Para volver a verde, completa el siguiente código en el método vacío addTask():

1
<?php
2
3
namespace Petersuhm\Todo;
4
5
class TodoList
6
{
7
    public $tasks;
8
9
    public function addTask(Task $task)
10
    {
11
        $this->tasks->add($task);
12
    }
13
}

¡De vuelta al verde! Se siente bien, ¿verdad?

Comprobación de tareas

Echemos un vistazo también a las promesas. Queremos un método que pueda decirnos si hay tareas en la colección. Para esto, simplemente verificaremos el valor de retorno del método count() en la colección. Nuevamente, no necesitamos una instancia real con un método count() real. Solo tenemos que asegurarnos de que nuestro código llame a algún método count() y haz algunas cosas dependiendo del valor de retorno.

Echa un vistazo al siguiente ejemplo:

1
function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
2
{
3
    $tasks->count()->willReturn(0);
4
    $this->tasks = $tasks;
5
6
    $this->hasTasks()->shouldReturn(false);
7
}

Tenemos un colaborador de colección de tareas que tiene un método count() que devolverá cero. Ésta es nuestra promesa. Lo que esto significa es que cada vez que alguien llama al método count(), devolverá cero. A continuación, asignamos el colaborador preparado a la propiedad $tasks en nuestro objeto. Por último, intentamos llamar a un método, hasTasks() y nos aseguramos de que devuelve false.

¿Qué tiene que decir phspec sobre esto?

1
$ vendor/bin/phpspec run
2
Do you want me to create `Petersuhm\Todo\TodoList::hasTasks()` for you? y
3
$ vendor/bin/phpspec run
4
5
        Petersuhm/Todo/TodoList
6
  25  ✘ checks whether it has any tasks
7
        expected false, but got null.

Genial. phpspec nos hizo un método hasTasks() y, como era de esperar, devuelve null, no false.

Una vez más, este es fácil de solucionar:

1
public function hasTasks()
2
{
3
    return false;
4
}

Volvemos al verde, pero esto no es exactamente lo que queremos. Revisemos las tareas cuando haya 20 de ellas. Esto debería volver a ser true:

1
function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
2
{
3
    $tasks->count()->willReturn(0);
4
    $this->tasks = $tasks;
5
6
    $this->hasTasks()->shouldReturn(false);
7
8
    $tasks->count()->willReturn(20);
9
    $this->tasks = $tasks;
10
11
    $this->hasTasks()->shouldReturn(true);
12
}

Ejecuta phspec y obtendremos:

1
$ vendor/bin/phpspec run
2
3
        Petersuhm/Todo/TodoList
4
  25  ✘ checks whether it has any tasks
5
        expected true, but got false.

De acuerdo, false no es true, por lo que debemos mejorar nuestro código. Usemos ese método count() para ver si hay tareas o no:

1
public function hasTasks()
2
{
3
    if ($this->tasks->count() > 0)
4
        return true;
5
6
    return false;
7
}

¡Y eso es todo! ¡Vuelve al verde!

Construyendo emparejadores personalizados

Parte de escribir buenas especificaciones es hacerlas lo más legibles posible. Nuestro último ejemplo se puede mejorar un poco, gracias a los emparejadores personalizados de phpspec. Es fácil implementar comparadores personalizados; todo lo que tenemos que hacer es sobrescribir el método getMatchers() que se hereda de ObjectBehavior. Al implementar dos comparadores personalizados, nuestra especificación se puede cambiar para que se vea así:

1
function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
2
{
3
    $tasks->count()->willReturn(0);
4
    $this->tasks = $tasks;
5
6
    $this->hasTasks()->shouldBeFalse();
7
8
    $tasks->count()->willReturn(20);
9
    $this->tasks = $tasks;
10
11
    $this->hasTasks()->shouldBeTrue();
12
}
13
14
function getMatchers()
15
{
16
    return [
17
        'beTrue' => function($subject) {
18
            return $subject === true;
19
        },
20
        'beFalse' => function($subject) {
21
            return $subject === false;
22
        },
23
    ];
24
}

Creo que esto se ve bastante bien. Recuerda que es importante refactorizar tus especificaciones para mantenerlas actualizadas. La implementación de tus propios comparadores personalizados puede limpiar tus especificaciones y hacerlas más legibles.

En realidad, también podemos usar la negación de los emparejadores:

1
function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
2
{
3
    $tasks->count()->willReturn(0);
4
    $this->tasks = $tasks;
5
6
    $this->hasTasks()->shouldNotBeTrue();
7
8
    $tasks->count()->willReturn(20);
9
    $this->tasks = $tasks;
10
11
    $this->hasTasks()->shouldNotBeFalse();
12
}

Sí. ¡Muy genial!

Conclusión

Todas nuestras especificaciones son verdes, ¡y mira lo bien que documentan nuestro código!

1
      Petersuhm\Todo\TaskCollection
2
3
  10  ✔ is initializable
4
  15  ✔ adds a task to the collection
5
  24  ✔ is countable
6
  29  ✔ counts elements of the collection
7
8
      Petersuhm\Todo\Task
9
10
  10  ✔ is initializable
11
12
      Petersuhm\Todo\TodoList
13
14
  11  ✔ is initializable
15
  16  ✔ adds a task to the list
16
  24  ✔ checks whether it has any tasks
17
18
19
3 specs
20
8 examples (8 passed)
21
16ms

Hemos descrito y logrado de manera efectiva el comportamiento deseado de nuestro código. Sin mencionar que nuestro código está cubierto al 100% por nuestras especificaciones, lo que significa que la refactorización no será una experiencia que induzca al miedo.

Al seguirlo, espero que te hayas inspirado para probar phpspec. Es más que una herramienta de prueba, es una herramienta de diseño. Una vez que te acostumbres a usar phpspec (y sus increíbles herramientas de generación de código), ¡tendrás dificultades para dejarlo de nuevo! Las personas a menudo se quejan de que hacer TDD o BDD los ralentiza. Después de incorporar phpspec en mi flujo de trabajo, realmente siento lo contrario: mi productividad ha mejorado significativamente. ¡Y mi código es más sólido!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.