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.

Testes Automatizados e TDD - Parte 3
Photo by Jeswin Thomas on Unsplash

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:

  1. Fast - Os testes devem ser rápidos
  2. Independent - Um teste não deve depender de outro, eles devem poder ser executados de maneira isolada
  3. Repeatable - Os testes devem poder serem executados inúmeras vezes sem que haja alteração no seu resultado
  4. Self-validating - O próprio teste deve ter uma saída bem definida e ser capaz de indicar se passou ou falhou
  5. 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());
  }
}
Exemplo de Dummy

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);
  }
}
MovieApi.php

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());
  }
MovieCatalogTest.php

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());
  }
MovieCatalogTest.php

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);
  }
MovieCatalogTest.php

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());
  }
MovieCatalog.php

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á!