Atualmente, muitas empresas adotam testes unitários em PHP para melhorar a qualidade do código desenvolvido e aumentar a confiabilidade dos sistemas. Certamente, por essas ferramentas desempenharem um papel fundamental ao reduzir problemas de dívidas técnicas, um desafio constante na engenharia de software.
Categorias de Testes de Software
Existem diferentes categorias de testes para diversos estágios do desenvolvimento. Elas variam desde os testes unitários, usados principalmente por desenvolvedores, até os testes de aceitação, que envolvem a participação e aprovação do cliente.
Ferramentas como PHPUnit são essenciais para implementar testes unitários eficazes em PHP, garantindo a qualidade do código durante todo o ciclo de desenvolvimento. O PHPUnit é um framework amplamente utilizado que facilita a criação e execução de testes, garantindo a qualidade do código durante todo o ciclo de desenvolvimento.
Estrutura dos Testes Unitários
Este conteúdo foca em testes unitários aplicados à programação orientada a objetos em PHP. Alguns detalhes também se aplicam a testes funcionais. Contudo, vamos abordar outras categorias de testes em artigos futuros.
Para começar, é interessante entender a estrutura dos testes unitários. Então, todo teste unitário, que você implementa como método em uma classe de testes, segue três etapas principais:
- Preparação: Aqui, você gera a instância da classe que você vai testar. Como muitas classes possuem dependências, você resolve essas dependências nessa fase.
- Execução: Após preparar tudo, você executa o teste invocando o método alvo e, assim, compara o resultado ou comportamento obtido com o valor ou comportamento esperado.
- Asserção: Na última etapa, você verifica se o resultado da execução é o esperado.
A ordem das etapas nem sempre é tão óbvia, principalmente em testes com asserções comportamentais, mas abordaremos isso em outro artigo.
O teste deve ser de uma unidade mínima testável
Nos testes unitários em PHP, o foco deve ser sempre testar apenas elementos de uma classe de cada vez. Sendo assim, geralmente, não há necessidade de invocar mais de um método para alcançar o resultado desejado. O teste unitário, como o próprio nome indica, se concentra em uma unidade mínima da classe alvo, limitando-se, no máximo, ao nível de classe. Entretanto, se você incluir outras classes, o teste deixa de ser unitário e acaba se transformando em um teste de integração. Ainda assim, existem exceções, especialmente quando utilizamos dublês de teste.
Evitando Testes Viciados
Evite criar testes viciados! Usar uma única asserção pode não ser suficiente para garantir a confiabilidade. Para cobrir todos os cenários, você deve criar testes que abrangem a maioria das situações possíveis. Por exemplo, em uma classe de validação que retorna um valor booleano, você deve testar tanto o retorno verdadeiro quanto o falso.
Mesmo que a classe de validação tenha apenas dois estados possíveis (verdadeiro ou falso), você pode criar pelo menos três cenários de teste:
- Um cenário em que nenhum valor é fornecido, resultando também em falha na validação (retorno
false
). - Um cenário que forneça um valor correto, resultando em validação bem-sucedida (retorno
true
). - Um cenário que forneça um valor incorreto, resultando em falha na validação (retorno
false
).
Em testes unitários, você sempre foca em testar apenas uma única classe. No entanto, como você pode lidar com as dependências dessa classe sem comprometer o teste? A solução ideal é utilizar dublês de objetos, que simulam o comportamento de objetos reais. Esses dublês permitem que você injete dependências e, ao mesmo tempo, evite testar diretamente as classes externas. Embora, à primeira vista, isso pareça contraditório, criar uma classe dublê para retornar valores estáticos ainda mantém o teste no nível unitário. Afinal, você testa somente a lógica da classe principal, sem envolver suas dependências. Nos próximos artigos, exploraremos mais a fundo o uso de dublês em diferentes cenários.
Como os dublês funcionam?
Agora, vamos abordar os dublês de teste (Test Doubles). Eles simulam comportamentos de dependências, como classes externas ou componentes complexos que você não precisa testar diretamente no teste unitário.
O termo 'Test Double' é amplamente usado tanto na literatura quanto na prática de testes de software, referindo-se a qualquer objeto que substitui um componente real durante a execução de um teste. Além disso, Gerard Meszaros, em seu livro 'xUnit Test Patterns', foi quem popularizou esse conceito, consolidando-o como um recurso essencial em testes unitários.
O dublê: Dummy
Suponha que você esteja testando o método setDescricao
da classe Produto
. Acompanhe essa idéia olhando para classe que se segue após esse paragráfo. Então, primeiramente, para instanciar a classe Produto você deve resolver a dependência Connection
durante a fase de preparação. No entanto, embora o método setDescricao
seja o foco do teste, note que ele não utiliza nenhum método do objeto Connection
, que deveria ser injetado como dependência. Neste caso, o dublê, conhecido como Dummy, será injetado e apenas resolverá a dependência no construtor, sem interferir diretamente no teste.
<?php
namespace App\Models\Cadastro;
use Framework\Core\System\Model;
use Framework\Core\Database\Connection;
class Produto extends Model
{
protected Connection $conn;
protected array $data = [];
public function __construct(Connection $conn)
{
$this->conn = $conn;
}
public function setDescricao (string $descricao): Produto
{
$this->data['descricao'] = strtoupper($descricao);
return $this;
}
}
Aqui está a classe de teste para a classe Produto
, utilizando um Dummy para resolver a dependência da classe Connection
. O Dummy será usado apenas para injetar a dependência sem influenciar o comportamento do método setDescricao
.
<?php
use PHPUnit\Framework\TestCase;
use App\Models\Cadastro\Produto;
use Framework\Core\Database\Connection;
class ProdutoTest extends TestCase
{
protected $connectionDummy;
protected $produto;
protected function setUp(): void
{
// Cria um Dummy para a dependência Connection
$this->connectionDummy = $this->createMock(Connection::class);
// Instancia a classe Produto com o Dummy de Connection
$this->produto = new Produto($this->connectionDummy);
}
/**
* Teste para verificar se a descrição é convertida para maiúsculas.
*/
public function testSetDescricaoConverteDescricaoParaMaiusculas(): void
{
// Define uma descrição com letras minúsculas
$descricao = 'carro';
// Chama o método setDescricao
$this->produto->setDescricao($descricao);
// Verifica se a descrição foi convertida para maiúsculas
$this->assertEquals('CARRO', $this->produto->data['descricao']);
}
/**
* Teste para verificar o retorno encadeado do método setDescricao.
*/
public function testSetDescricaoRetornaInstanciaDeProduto(): void
{
// Define uma descrição qualquer
$descricao = 'bicicleta';
// Chama o método setDescricao e verifica se ele retorna a própria instância de Produto
$this->assertInstanceOf(Produto::class, $this->produto->setDescricao($descricao));
}
}
Agora, suponha que a dependência injetada realmente desempenhe um papel ativo no método que você está testando. Observe essa outra classe a seguir:
<?php
namespace App\Validations\Comentarios;
use App\Repositories\Comentarios\ListaNegraRepository;
class ComentarioValidator
{
protected ListaNegraRepository $listaNegraRepository;
public function __construct(ListaNegraRepository $listaNegraRepository)
{
$this->listaNegraRepository = $listaNegraRepository;
}
public function verficarSeExistePalavraProibida(string $comentario): bool
{
$palavrasProibidas = $listaNegraRepository->getAllAsArray();
foreach($palavrasProibidas as $palavra)
{
if($this->encontrar($palavra, $comentario)) {
return true;
}
}
return false;
}
private function encontrar(string $palavra, string $comentario): bool
{
return !! strpos($palavra, $comentario);
}
}
O dublê: Mock
Nesse caso, usamos um Mock para simular o comportamento daquele método que faz parte da dependência. Portanto, neste caso, a dependência precisará ser preparada para se comportar como o objeto real. O Mock será definido dessa maneira na fase de preparação do teste, pois o objeto injetado é essencial para o funcionamento do método verificarSeExistePalavraProibida
.
Ainda neste exemplo, ao testar o método, você passará uma string como argumento e obterá um valor booleano como resultado, que poderá ser verdadeiro ou falso dependendo do processamento. Então, o Mock simulará também o método getAllAsArray
da classe ListaNegraRepository
, que retornará um array de palavras proibidas. Você poderá iterar sobre esse array, e o método encontrar
verificará se cada palavra está presente no comentário. Se encontrar uma correspondência, ele retorna true; caso contrário, retorna false.
Nesse caso, o dublê, conhecido como Mock, simulará todo o comportamento da dependência, permitindo criar cenários variados que cobrem diferentes situações. Veja rapidamente um exemplo de criação de Mocks no PHPUnit:
$mock = $this->createMock(ListaNegraRepository::class);
$mock->method('getAllAsArray')->willReturn(['proibida', 'banida']);
Aqui estão quatro cenários possíveis:
- Um valor correto é fornecido, a validação falha e retorna
false
. - Um valor incorreto é fornecido, a validação tem sucesso e retorna
true
. - Nenhum valor é fornecido, a validação falha e retorna
false
. - Um valor é fornecido, mas não há dados no banco, resultando em falha e retorno
false
.
Esses cenários abrangem a maioria das situações, mas ainda podem não cobrir todas as possibilidades.
Aqui está a classe de teste para a classe ComentarioValidator
utilizando PHPUnit e Mocks. O objetivo é testar o comportamento do método verficarSeExistePalavraProibida
utilizando diferentes cenários de teste, com base no comportamento do mock de ListaNegraRepository
.
<?php
use PHPUnit\Framework\TestCase;
use App\Validations\Comentarios\ComentarioValidator;
use App\Repositories\Comentarios\ListaNegraRepository;
class ComentarioValidatorTest extends TestCase
{
protected $listaNegraRepositoryMock;
protected $comentarioValidator;
protected function setUp(): void
{
// Cria um Mock para a ListaNegraRepository
$this->listaNegraRepositoryMock = $this->createMock(ListaNegraRepository::class);
// Injeta o Mock na instância de ComentarioValidator
$this->comentarioValidator = new ComentarioValidator($this->listaNegraRepositoryMock);
}
/**
* Cenário 1: Um valor correto é fornecido, a validação falha e retorna false.
*/
public function testValidacaoFalhaComPalavraCorreta(): void
{
// Define o comportamento do Mock para retornar palavras proibidas
$this->listaNegraRepositoryMock
->method('getAllAsArray')
->willReturn(['proibida', 'banida']);
// Comenta uma palavra permitida que não está na lista
$comentario = 'Esse comentário está limpo';
// Executa o teste e espera retorno false
$this->assertFalse($this->comentarioValidator->verficarSeExistePalavraProibida($comentario));
}
/**
* Cenário 2: Um valor incorreto é fornecido, a validação tem sucesso e retorna true.
*/
public function testValidacaoSucessoComPalavraProibida(): void
{
// Define o comportamento do Mock para retornar palavras proibidas
$this->listaNegraRepositoryMock
->method('getAllAsArray')
->willReturn(['proibida', 'banida']);
// Comenta uma palavra proibida
$comentario = 'Esse comentário contém a palavra proibida';
// Executa o teste e espera retorno true
$this->assertTrue($this->comentarioValidator->verficarSeExistePalavraProibida($comentario));
}
/**
* Cenário 3: Nenhum valor é fornecido, a validação falha e retorna false.
*/
public function testValidacaoFalhaSemValor(): void
{
// Define o comportamento do Mock para retornar palavras proibidas
$this->listaNegraRepositoryMock
->method('getAllAsArray')
->willReturn(['proibida', 'banida']);
// Comentário vazio
$comentario = '';
// Executa o teste e espera retorno false
$this->assertFalse($this->comentarioValidator->verficarSeExistePalavraProibida($comentario));
}
/**
* Cenário 4: Um valor é fornecido, mas não há dados no banco, resultando em falha e retorno false.
*/
public function testValidacaoFalhaSemPalavrasNoBanco(): void
{
// Define o comportamento do Mock para retornar um array vazio
$this->listaNegraRepositoryMock
->method('getAllAsArray')
->willReturn([]);
// Comenta algo
$comentario = 'Esse comentário deve falhar, mas o banco está vazio';
// Executa o teste e espera retorno false
$this->assertFalse($this->comentarioValidator->verficarSeExistePalavraProibida($comentario));
}
}
Outros Tipos de Dublês: Spy e Stub
Além de Mocks e Dummies, você pode usar outros dublês de teste em cenários mais complexos.
Spy: Monitora comportamentos, agindo como um espião que verifica quando o código chama métodos e como ele realiza essas chamadas. Dessa forma, ele se torna ideal para testar interações e verificar se o sistema usa as dependências corretamente.
Veja um exemplo de um Spy criado com PHPUnit:
$spy = $this->getMockBuilder(ClasseAlvo::class)
->onlyMethods(['metodoAlvo'])
->getMock();
$spy->expects($this->once())
->method('metodoAlvo')
->with($this->equalTo('parametro'));
Stub: Um stub é usado para retornar valores estáticos ou predefinidos em testes. Por exemplo, você pode usá-lo para simular operações de leitura ou escrita no banco de dados sem realmente interagir com ele.
Veja um exemplo bem breve de um Stub simulando uma chamada para uma API Externa:
- Primeiramente, criamos um arquivo JSON com os dados simulados:
{
"id": 1,
"nome": "Usuário de Teste",
"email": "usuario@email-exemplo.com.br"
}
- Em seguida, usamos o stub no teste carregando o arquivo JSON no lugar da chamada real:
class UsuarioServiceTest extends TestCase
{
public function testBuscarUsuarioComStub()
{
$stubData = file_get_contents(__DIR__ . '/stubs/usuario_stub.json');
$usuarioStub = json_decode($stubData, true);
$usuarioService = $this->createMock(UsuarioService::class);
$usuarioService->method('buscarUsuario')->willReturn($usuarioStub);
$usuario = $usuarioService->buscarUsuario(1);
$this->assertEquals('Usuário de Teste', $usuario['nome']);
}
}
Conclusão
Os testes unitários em PHP são essenciais para garantir a qualidade e a confiabilidade dos sistemas, especialmente quando você utiliza dublês como Mock, Dummy, Stub, e Spy. Dominar esses conceitos é fundamental para construir testes eficazes, cobrindo a maioria dos cenários e prevenindo erros antes de chegarem à produção.
Quer aprender mais sobre dublês e testes unitários? Continue acompanhando nossos próximos artigos! Para mais detalhes, você pode conferir a Documentação oficial do PHPUnit e a Documentação do Laravel.
[]’s
Saiba mais:
- Meszaros, Gerard. xUnit Test Patterns: Refactoring Test Code. Addison-Wesley Professional, 2007.
- Martin Fowler: Mocks Aren’t Stubs — Um artigo do Martin Fowler que também aborda o conceito de Test Doubles, explicando a diferença entre Mocks, Stubs e outros tipos.