Advertisement
  1. Code
  2. Web Development

Primeiros Passos com Phpspec

Scroll to top
Read Time: 16 min

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Nesse pequeno, porém fácil, tutorial, nós falaremos sobre desenvolvimento movido a comportamento (do inglês, Behavior Drive Development, ou BDD), com phpspec. No geral, será uma introdução à ferramenta phpspec, mas, enquanto a apresentemos, falaremos sobre os diferentes conceitos de BDDBDD é um dos assuntos do momento e o phpspec ganhou bastante atenção na comunidade PHP, recentemente.

SpecBDD & Phpspec

DBB é sobre descrever o comportamente do aplicativo, de modo que tenhamos o projeto da forma correta. Ele é, geralmente, associado ao TDD (do inglês, Test Driven Development, ou, em português, desenvolvimento movido a testes), mas, enquanto o TDD foca em testarsua aplicação, o BDD está mais para descrever o comportamento da mesma. Usar a abordagem do BDD forçará você a considerar, constantemente, os requerimentos verdadeiros e o comportamente desejado do software que está construindo.

Duas ferramentas para BDD ganharam bastante atenção da comunidade PHP, recentemente: Behat e Phpspec. Behat ajuda você a descrever ocomportamente externo da sua aplicação, usando a linguagem de fácil leitura, Gherkin. phpspec, por sua vez, ajuda a descrever ocomportamente interno da sua aplicação, ao escrever pequenas "especificações" usando PHP - daí o nome SpecBDD (Spec de specification). Essas especificações testam se seu código tem o comportamente desejado.

O Que Nós Faremos

Nesse tutorial, cobriremos tudo relacionado a começa com o phpspec. No caminho, criaremos a base de uma aplicação do tipo "lista de tarefas", passo-a-passo, usando a abordagem ddo SpecBDD. Enquanto programamos, lançaremos mão do phpspec.

Nota: Esse é um artigo intermediário sobre PHP. Estou assumindo que você tem conhecimento suficiente sobre orientação a objetos com PHP

Instalação

Para esse tutorial, assumiremos que você tem as seguintes ferramentas instaladas e funcionando:

  • Ambiente de desenvolvimento PHP (versão mínima 5.3)
  • Composer

Instalar o phpspec usado Composer é a forma mais fácil. Tudo que você precisa fazer é rodar o seguinte comando no seu terminal:

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

Para garantir que tudo esteja em seu devido lugar e funcionando, rode o comando phpspec e veja se obtém o seguinte retorno:

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

Configuração

Antes de começarmos, nós precisamos fazer pequenas modificações na configuração. Quando phpspec rodar, ele procurará por um arquivo chamado phpspec.yml. Já que usaremos namespace em nossos códigos, precisamos fazer com que o phpspec saiba disso. Aproveitaremos, também, para garantir que nossas especificações apareçam de forma fácil e bonita de se ver quando mandarmos executá-las.

Vá em frente e crie o arquivo com o conteúdo a seguir:

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

Há inúmeras outras opções de configuração. Você pode ler mais sobre elas na documentação.

Outra coisa que precisamos fazer, é dizer ao Composer para carregar automaticamente nosso código. phpspec usará o autoarregador do Composer, logo, isso é obrigatório que nossas especificações executem.

Adicione um elemento autoload ao arquivo composer.json que o Composer criou para você:

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

Executar o comando composer dump-autoload atualizará o autocarregador após essa mudança.

Nossa Primeira Especificação

Agora, que já estamos prontos, é hora de escrever nossa primeira especificação. Começaremos descrevendo a classe chamadaTaskCollection. Faremos com que phpspec gere uma classe de especificação para nós, usando o comendo describe (ou a versão abreviada, desc).

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

O que acabou de acontecer aqui? Primeiro, nós pedimos para o phpspec criar a classe TaskCollection. Segundo, nós executamos o conjunto de especificações e, automagicamente, o phpspec ofereceu para criar a classe TaskCollection para nós. Legal, não é?

Vá em frente, rode o conjunto novamente e verá que já temos um exemplo em nossas especificações (logo veremos o que é um exemplo):

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

Desse retorno, podemos ver que a classe TaskCollection éinicializável. O que isso significa? Veja o arquivo de especificação que o phpspec criou e tudo ficará 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
}

A frase 'is initializable' é detivada da função it_is_initializable(), a qual o phpspec adiciona à classe TaskCollectionSpec. Essa função é o que nós chamamos de exemplo (example, no retorno anterior). Nesse exemplo em particular, nós temos o que podemos chamar de combinador, chamado de shouldHaveType(), que verifica o tipo da nossa classe TaskCollection. Se você mudar o parâmetro passado para essa função para qualquer outra coisa e roda a especificação novamente, verá que ela falhará. Antes de entender isso completamente, preciamos descobrir a que, precisamente, se refere a variável $thisem nossa especificação.

O Que É $this?

Obviamente, $this refere-se à instância da classeTaskCollectionSpec, uma vez que isso, nada mais é, que um código PHP comum. Mas, com phpspec, você tem de tratar o $this de forma diferente com o que está acostumado, já que, por baixo dos panos, ele se refere, na verdade, ao objeto em teste, que, nesse caso, é a classeTaskCollection. Esse comportamente é herdado da classeObjectBehavior, que garante que as chamadas às funções sejam redirecionadas (através de um Proxy) à classe verdadeira. Isso significa que AlgumaClasseSpec redirecionará as chamadas dos métodos para uma instância de AlgumaClasse. phpspec envolverá essas chamadas de métodos para garantir que os seus resultados sejam rodados a algum combinador como o que você acabou de ver.

Você não precisa entender perfeitamente isso para usar o phpspec, apenas se lembre que a $this, na verdade, refere-se ao objeto sob teste.

Construindo Nossa Coleção de Tarefas

Até agora, não fizemos muita coisas por conta própria. Mas o phpspec criou uma classe TaskCollection para nós usarmos. Agora, é hora de escrever um pouco de código e tornar essa classe útil. Nós adicionaremos dois métodos: um add(), para adicionar tarefas, e umcount(), para contar o número de tarefas na coleção.

Adicionando uma Tarefa

Antes de escrevermos qualquer código de verdade, nós deveríamos criar um exemplo em nossa especificação. Em nosso exemplo, nós queremos tentar adicionar uma tarefa à nossa coleção e, então, garantir que essa tarega foi adicionada de fato. Para fazer isso, ´precismos de uma instância da classe Task (ainda não existente). Se adicionarmos essa dependência como parâmetro para a nossa função de especificação, o phpspec nos dará, automaticamente, uma instância para que possamos usar. Na verdade, a instância não é de verdade, mas algo que o phpspec identifica como Collaborator. Esse objeto agirá como o verdadeiro objeto, mas o phpspec permitirá que façamos muitas coisas com ele. Algo que veremos em breve, inclusive. Embora a classe Task não exista ainda, por hora, finja que ela existe. Abra a classeTaskCollectionSpec e adicione a instrução use para adicionar a classe Task e, depois, adicionae o exemploit_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
}

Em nosso exemplo, escrevemos o código que "gostaríamos de ter". Nós chamamos o método call() e então tentamos passá-lo uma $task. Checamos, então, se a tarefa foi, de fato, adicionada à variável de instância, $tasks. O combinador shouldBe() é um combinador de identidade idêntico ao comparador === do PHP. Você usar tanto oshouldBe()shouldBeEqualTo()shouldEqual() oushouldReturn() - todos fazem a mesma coisa.

Ao executar o phpspec, teremos alguns erros, já que não temos a classeTask ainda.

Façamos com que o phpspec arrume isso para nós:

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

Executando o phpspec novamente, algo interessante acontece:

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

Perfeito! Se você for olhar o arquivo TaskCollection.php, verá que o phpspec criou uma função add() para que a preenchamos.

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

10
}
11
}

Porém, phpspec ainda reclama. Nós não temos nossa array $tasks. Então, vamos criá-ça e adicionar nossa tarefa a ela:

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
}

Agora, nossas especificações estão legais e todas verdes. Note que fiz questão de lançar mão da checagem de tipos no parâmetro $task;

Só para garantir que nós fizemos tudo certo, vamos adicionar outra tarefa:

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
}

Ao rodar phpspec, tudo estará legal.

Implementando a Interface Countable

Querer saber quantas taremos existem em uma coleção é uma excelente razão para usaarmos uma das interfaces da Bbiblioteca Padrão do PHP(do inglês, Standard PHP Library, SPL), mais especificamente aCountable interface.

Mais cedo, nós usamos o combinador shouldHaveType(), que é um combinador de tipo. Ele usa o comparador instanceof do PHP para validar se um objeto é uma instância de uma dada classe. Há quatro combinadores de tipo, os quais fazem a mesma coisa. Um deles é oshouldImplement(), que é perfeito para nosso objetivo, então vamos usá-lo em nosso exemplo:

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

Você percebe o quão bonito e simple é de ler? Vamos executar o exemplo e deixar que o phpspec nos guie:

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].

Certo, então. Nosso código não é uma instância de Countable, uma vez que não a implementamos ainda. Vamos atualizar o código da nossa class TaskCollection:

1
class TaskCollection implements \Countable

Nossos testes não executarão, já que a interface Countable tem um método abstrato, count(), que nós devemos implementar. Um método vazia funcionará, por hora:

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

4
}

E voltamos ao verde. Nesse momento, nosso método count() não faz muita coisa e, na verdade, é bem inútil. Escrevamos uma especificação para o comportamente que desejamos que ele tenha. Primeiro, sem tarefas, é esperado retornar zero de nossa função:

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

It returns null, não 0. Para fazermos o teste ficar verde, vamos conserta-la ao modo TDD/BDD:

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

Agora, estamos verde e tudo está bem, exceto que esse não é o provável comportamento que queremos. Ao invés disso, expandamos nossa especificação e adicionemos algo à array $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
}

Obviamente, nosso código ainda retorna 0 e nós temos um passo em vermelho. Ajustar isso não é muito difícil e nossa class TaskCollectiondeve parecer com isso, agora:

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

Agora, nós temos um teste verde e nosso método count() funciona. Que legal!

Expectativas e Promessas

Você se lembra que eu falei que o phpspec permite você fazer várias coisas legais com as intâncias da classe Collaborator, também conhecidas como as instâncias que são injetadas automaticamente pelo phpspec? Se você já escreveu testes antes, você sabe o que são os objetos simulados (mocks) e as funções substitutas (stubs). Se você não sabe, não se preocupe quanto a isso. São só jargões. Eles se referem ao objetos "falsos" que agirão no lugar dos objetos de verdade, que permitem você fazer testes isolados. phpspec, automaticamente, transforma as instâncias de Collaborator em mocks e stubs se você precisar em seus objetos.

Isso é incrível. Por trás, phpspec usa a biblioteca Prophecy, que é um arcabouço (framework) bem particular que trabalha bem com phpspec (e é construída pelos mesmos criadores do phpspec). Você pode estabelecer uma expectativa em um colaborador (mocking), algo como "esse método deveria ser invocado" e pode adicionar promessas (stubbing), como "esse método retornará esse valor". Com o phpspec, essa tarefa é bem simples e nós faremos as duas, logo a seguir.

Criemos uma classe, chamada de TodoList, que faz uso de nossa coleção:

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

Adicionando Tarefas

O primeiro exemplo que adicionaremos é um para adicionar tarefas. Nós criaremos um método addTask(), que nada mais faz que adicionar uma tarefa à nossa coleção. Ele simplesmente direciona a chamada ao método add() da coleção, logo, aqui é um lugar perfeito para usar uma Expectativa. Nós não queremos invocar o método add() de verdade, nós só queremos garantir que ele tente fazê-lo. Além do mais, nós queremos garantir que ele seja chamado apenas uma vez. Veja como podemos faz isso com 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 $taks)
18
{
19
$tasks->add($task)->shouldBeCalledTimes(1);
20
$this->tasks = $tasks;
21
 
22
$this->addTask($task);
23
}
24
}

Primeiro, fazemos com phpspec proveja dois colaboradores que precisamos: uma coleção de tarefas e uma tarefa. Depois disso, estabelecemos uma expectativa no colaborador da coleção de tarefas que, basicamente, diz: "O método add() deverá ser chamado, exatamente, 1 vez com a variável $task como parâmetro". É assim que preparamos nosso colaborador, que agora é um mock, antes de atribuí-lo à propriedade $tasks de TodoList. Finalmente, nós tentamos invocar o método addTask() de verdade.

Certo, o que o phpspec tem a dizer sobre isso:

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

A propriedade $tasks não existe. Ajustar esse problema é bem fácil:

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

Tente novamente e deixe o phpspec ser nosso guia:

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.

Okay, algo interessante aconteceu agora. Viua mensagem "Expected exactly 1 calls that match: ...."? Essa é nossa expectativa falha. Isso acontece porque, depois de invocar o método addTask(), o métodoadd() na coleção não foi invocado, o que era esperado acontecer.

Para voltar a termos tudo verde, coloque o seguinte código dentro do método 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
}

Tudo verde, novamente. Isso é muito bom, não é?

Verificando Tarefas

Façamos algo relacionado a promessas, também. Nós queremos um método que nos diga se há alguma tarefa em nossa coleção. Para isso, simplesmente verificaremos o valor de retorno do método count() da coleção. Novamente, nós não precisaremos de uma instância de verdade com um método count() de verdade. Nós só precisamos garantir que nosso código chame algum método count() e realize alguma tarefa, dependendo do valor de retorno do método.

Veja o seguinte exemplo:

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
}

Nós temos um colaborador de coleção de tarefas que tem um métodocount() que retornará zero. Essa é nossa promessa. Isso significa que, toda vez que alguém chamar o método count(), ele retornará zero. Nós, então, atribuímos o colaborador preparado à propriedade $tasksdo nosso objeto. Por fim, nós tentamos chamar um método,hasTasks(), e garantir que ele retorna false.

O que a especificação tem a nos dizer sobre isso?

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.

Legal. phpspec criou para nós um método hasTasks() e, sem surpresas, ele retorna null e não false.

Novamente, essa é uma tarefa fácil de resolver:

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

Obtivemos tudo verde, mas, isso não é bem o que queremos. Verifiquemos pelas tarefas quando tivermos 20 delas. Dessa vez, deverá retornar true:

1
function it_checks_wheter_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
}

Execute phpspec e você terá:

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

Certo, false não é true, então, precisamos melhorar nosso código. Vamos usar o método count() para verificar se há tarefas ou não:

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

Tah dah! Tudo verde, de novo!

Construindo Combinadores Customizados

Parte de criar boas especificações é fazê-las o mais legível possível. Nosso último exemplo pode, na verdade, ser aprimorado um pouco, graças aos combinadores customizados do phpspec. É fácil de implementar combinadores customizados -tudo que temos de fazer é sobrescrever o método getMatchers() que é herdado deObjectBehavior. Ao implementarmos dois combinadores customizados, nossa especificação pode ser alterada para parecer com isso:

1
function it_checks_wheter_it_has_any_tasks(TaskCollection $tasks)
2
{
3
$tasks->count()->willReturn(0);
4
$this->tasks = $tasks;
5
 
6
$this->hasTasks()->shouldBeFalse();
7
 
8
$this->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
}

Eu acredito que isso esteja parecendo bem bacana. Lembre-se que refatorar suas especificações é importante para mante-las atualizadas. Implementar seus próprios combinadores customizados pode limpar suas especificações e torná-las mais legíveis.

Na verdade, nós podemos usar a negação dos combinadores também:

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
}

Bem legal!

Conclusão

Todas as nossas especificações estão verdes e veja como elas documentam de forma legal nosso 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

Nós descrevemos de forma efetiva e alcançamos o comportamento desejado de nosso código. Sem mencionar que, uma vez que nosso código está coberto por nossas especificações, a refatoração do mesmo não será uma experiência pavorosa.

Por ter seguido esse tutorial, espero ter inspirado você a dar uma chance ao phpspec. Ele é mais que uma ferramenta de testes - é uma ferramenta de design. Uma vez acostumado a usar o phpspec (e suas maravilhosas ferramentas de geração de código), você terá problemas em largar dele novamente! As pessoas, frequentemente, reclamam que seguir o caminho TDD ou BDD diminui a velocidade deles. Depois de incorporar o phpspec no meu fluxo de trabalho, eu sinto exatamente o contrário - minha produtividade melhorou bastante. Sem falar que meu código está muito mais sólido!

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

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.