Testes Automatizados e TDD - Parte 3
Na terceira parte dessa série sobre testes automatizados e TDD falaremos sobre as características que um teste deve possuir e dublês de teste.

Dando continuidade na nossa série sobre testes automatizados e TDD falaremos um pouco sobre os dublês de teste, seus tipos e quando utilizar cada um.
E caso você ainda não tenha visto as partes anteriores dessa série, recomendo dar uma olhada:
FIRST
Antes de avançarmos nos dublês de teste é importante ressaltar as características que um (bom) teste deve possuir, para isso utilizamos a sigla FIRST:
- Fast - Os testes devem ser rápidos
- Independent - Um teste não deve depender de outro, eles devem poder ser executados de maneira isolada
- Repeatable - Os testes devem poder serem executados inúmeras vezes sem que haja alteração no seu resultado
- Self-validating - O próprio teste deve ter uma saída bem definida e ser capaz de indicar se passou ou falhou
- Timely - Os testes devem ser escritos antes do código-fonte (aplicável ao TDD)
Dublê de teste
Test double ou dublê de teste é um padrão de teste (test pattern) que tem como objetivo substituir uma implementação por motivos de performance ou segurança. Podemos dividir os test patterns em: dummy, stubs, spies, mocks e fake. Não se preocupe, mostraremos cada um deles a seguir.
Muitas bibliotecas tratam os tests patterns como Mock
, portanto é comum encontrar bibliotecas do tipo Mockery
no PHP, ou mesmo métodos no PHPUnit como createMock
sendo utilizado para criar um Spy
, mas lembre-se cada padrão desempenha um papel diferente.
Dummy
Podemos dizer que Dummy é o test pattern mais simples: dummies são objetos que criamos apenas para completar a lista de parâmetros necessários para invocar determinado método.
Dando sequência a nossa aplicação de catálogo de filmes, vamos agora criar a classe MovieCatalog
que será nosso catálogo de filmes. Essa classe terá um método addMovie
que receberá um objeto do tipo Movie
e irá inseri-lo na lista.
Vamos ver como fica o teste:
<?php declare(strict_types=1);
class MovieCatalogTest extends TestCase
{
public function testShouldAddMovieToCatalog()
{
$catalog = new MovieCatalog();
$movie = new Movie(
id:'1',
name: 'Movie 1',
summary: ''
);
$catalog->addMovie($movie);
$this->assertNotEmpty($catalog->getMovies());
}
}
Nesse exemplo estamos criando um objeto do tipo Movie
com quaisquer informações apenas para passarmos para o método addMovie
uma vez que as informações contidas no objeto não são relevantes para a validação do nosso teste.
Stubs
Diferente dos dummies que são objetos sem comportamento, que servem apenas para completar os parâmetros necessários, os stubs são objetos que retornam respostas prontas para um determinado teste por questões de performance ou segurança.
Vamos supor que, quando um filme for inserido no catálogo iremos fazer uma requisição a uma API externa para obter a nota média da avaliação dos usuários para esse filme.
Para ilustrar criamos uma classe chamada MovieApi
que retorna um número aleatório a cada execução.
<?php declare(strict_types=1);
namespace App;
class MovieApi
{
public function getMovieRate(Movie $movie)
{
return rand(0, 5);
}
}
Cada vez que um filme é adicionado no catálogo, uma requisição à API é feita e a nota média do filme é incluída no catálogo. Incluímos aqui a MovieApi
como injeção de dependência, para facilitar nossos testes:
...
public function __construct(?MovieApi $movieApi = null)
{
$this->movieApi = $movieApi;
}
...
public function addMovie(Movie $movie)
{
$movieApi = new MovieApi;
$this->sumMovieRates += $movieApi->getMovieRate($movie);
array_push($this->movies, $movie);
}
Nosso teste quer validar que que a nota média dos filmes no catálogo é 4:
public function testShouldUpdateAvgRate()
{
$catalog = new MovieCatalog();
$movie = new Movie(
id: '1',
name: 'Movie 1',
summary: ''
);
$catalog->addMovie($movie);
$this->assertEquals(4, $catalog->getAvgRate());
}
Nesse caso se mantivermos a comunicação com uma API real estaremos ferindo o R do FIRST, uma vez que a cada execução obteremos um resultado diferente. Vamos ver como é possível resolver esse problema utilizando um Stub
:
public function testShouldUpdateAvgRate()
{
$stub = $this->createStub(MovieApi::class);
$stub->method('getMovieRate')
->willReturn(4);
$catalog = new MovieCatalog($stub);
$movie = new Movie(
id: '1',
name: 'Movie 1',
summary: ''
);
$catalog->addMovie($movie);
$this->assertEquals(4, $catalog->getAvgRate());
}
Após a alteração estamos indicando à nossa classe MovieCatalog
para utilizar um dublê da MovieApi
cujo método getMovieRate
sempre retornará o valor 4
.
Até o momento da escrita desse artigo, o PHPUnit não possuía uma forma nativa para criar stubs
de hard dependencies ou seja classes que são instanciadas diretamente na implementação. Para esses casos recomendamos a utilização da biblioteca Mockery
que fornece alguns poderes a mais ao PHPUnit.
Spies
Spies são objetos que espionam a execução do método armazenando seus resultados e alguns meta-dados como: quantas vezes foi chamado ou quais parâmetros foram passados.
Vamos duplicar nosso teste anterior para garantir que o método getMovieRate
da nossa API tenha sido chamado apenas uma vez.
NOTA: O PHPUnit não possui um método nativo para criação de spies
para isso utilizaremos o método createMock
que nos dá a possibilidade de espionar a execução do método.
public function testShouldCallGetMovieRateOnce()
{
$spy = $this->createMock(MovieApi::class);
$spy->expects($this->once())
->method('getMovieRate');
$catalog = new MovieCatalog($spy);
$movie = new Movie(
id: '1',
name: 'Movie 1',
summary: ''
);
$catalog->addMovie($movie);
}
Nesse exemplo se inserirmos uma chamada adicional ao método addMovie
o teste falhará pois o método getMovieRate
terá sido chamado mais de uma vez.
Mocks
Mocks são objetos similares a stubs
e spies
que permitem que você diga exatamente o que quer que ele faça.
Poderíamos evoluir o teste acima adicionando o comportamento que incluímos no exemplo do stub
, vamos ver como fica:
public function testShouldCallGetMovieRateOnce()
{
$mock = $this->createMock(MovieApi::class);
$mock->expects($this->once())
->method('getMovieRate')
->will($this->returnValue(4));
$catalog = new MovieCatalog($mock);
$movie = new Movie(
id: '1',
name: 'Movie 1',
summary: ''
);
$catalog->addMovie($movie);
$this->assertEquals(4, $catalog->getAvgRate());
}
Nesse exemplo combinamos o comportamento do stub
ao retornar um valor fixo e do spy
ao verificar a quantidade de vezes que o método foi chamado.
Fake
Fakes
são objetos cuja implementação simula o funcionamento da instância real que seria utilizada em produção. Como exemplo podemos citar um banco de dados em memória, ou mesmo a implementação da nossa API em nossos exemplos que retorna um número randômico simulando um possível retorno de uma API real.
Conclusão
Conhecemos o conceito de FIRST e tests patterns - padrões para escrever tests de maneira performática e segura e que não ferem o FIRST.
Apesar das limitações dos métodos nativos do PHPUnit é possível atingirmos o objetivo principal dos dublês de teste que é simular o comportamento da implementação real, sem correr riscos desnecessários de performance ou segurança.
Citamos também algumas bibliotecas (do PHP) que podem auxiliar nossos testes como Mockery
e prophecy
. Caso queira experimentar um pouco mais os tests patterns, recomendamos a utilização da bilioteca sinon
do JavaScript pois ela segue a nomenclatura e os conceitos das técnicas citadas nesse artigo.
Na próxima e última parte dessa série entraremos nos conceitos do TDD e como essa técnica pode nos ajudar a desenvolver códigos melhores. Nos vemos lá!