Mocking de dependências com PHPUnit
1. Introdução ao Mocking no PHPUnit
Testar unidades de código isoladamente é um dos pilares do desenvolvimento orientado a testes (TDD). No mundo real, classes raramente funcionam sozinhas — elas dependem de serviços externos, repositórios, APIs ou sistemas de arquivos. Mockar essas dependências no PHPUnit permite:
- Isolamento total: testar apenas a lógica da classe sob teste, sem efeitos colaterais.
- Previsibilidade: controlar exatamente o que uma dependência retorna ou como se comporta.
- Velocidade: evitar chamadas lentas a bancos de dados ou APIs externas.
Os dublês de teste se dividem em categorias:
- Stubs: fornecem respostas prontas para chamadas feitas durante o teste.
- Mocks: stubs que também verificam se determinados métodos foram chamados com argumentos específicos.
- Spies: registram interações para inspeção posterior.
- Fakes: implementações simplificadas que funcionam em memória (ex.: um repositório que usa array em vez de banco).
A regra de ouro: mockie apenas o que é externo e lento. Para objetos de valor ou entidades simples, prefira instâncias reais.
2. Criando Stubs com createStub()
O método createStub() gera um objeto que retorna valores fixos sem verificar quantas vezes foi chamado. Ideal para fornecer dados controlados.
<?php
use PHPUnit\Framework\TestCase;
class CadastroUsuarioTest extends TestCase
{
public function testCadastroComSucesso(): void
{
// Cria um stub do serviço de e-mail
$emailService = $this->createStub(EmailServiceInterface::class);
// Configura retorno fixo para o método enviar
$emailService->method('enviar')
->willReturn(true);
$cadastro = new CadastroUsuario($emailService);
$resultado = $cadastro->registrar('joao@exemplo.com', 'João');
$this->assertTrue($resultado);
}
}
No exemplo acima, o stub garante que enviar() sempre retorna true, independentemente dos argumentos. Isso isola o teste da lógica real de envio de e-mails.
3. Mock Objects com createMock()
Diferente dos stubs, os mocks com createMock() permitem verificar comportamento — quantas vezes um método foi chamado e com quais argumentos.
<?php
use PHPUnit\Framework\TestCase;
class RepositorioUsuarioTest extends TestCase
{
public function testSalvarUsuarioChamaMetodoSave(): void
{
// Cria um mock do repositório
$repositorio = $this->createMock(UsuarioRepositoryInterface::class);
// Espera que save() seja chamado exatamente 1 vez
$repositorio->expects($this->once())
->method('save')
->with($this->isInstanceOf(Usuario::class));
$servico = new ServicoUsuario($repositorio);
$usuario = new Usuario('maria@exemplo.com', 'Maria');
$servico->criarUsuario($usuario);
}
}
Os matchers de chamada mais comuns são:
$this->once()— exatamente uma chamada.$this->exactly(2)— exatamente duas chamadas.$this->never()— nunca deve ser chamado.$this->atLeastOnce()— pelo menos uma chamada.
4. Configuração Avançada de Expectativas
Matchers de argumentos com with()
$mock->expects($this->once())
->method('buscarPorEmail')
->with($this->stringContains('@exemplo.com'));
Retornos condicionais com willReturnCallback()
$mock->method('calcular')
->willReturnCallback(function ($a, $b) {
return $a + $b; // lógica personalizada
});
Lançamento de exceções
$stub->method('conectar')
->willThrowException(new \RuntimeException('Falha na conexão'));
withConsecutive() para chamadas sequenciais
$mock->expects($this->exactly(2))
->method('get')
->withConsecutive(['/api/v1'], ['/api/v2'])
->willReturnOnConsecutiveCalls(['status' => 'ok'], ['status' => 'erro']);
5. Mockando Métodos Estáticos e final
Métodos estáticos são um desafio porque o PHPUnit não pode substituí-los diretamente. Uma abordagem é usar a classe como dependência injetada:
class Logger
{
public static function log(string $mensagem): void { /* ... */ }
}
class Servico
{
private Logger $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function executar(): void
{
$this->logger::log('Iniciando...'); // chamada estática via instância
}
}
Para classes com construtores complexos, use disableOriginalConstructor():
$mock = $this->getMockBuilder(ClasseComplexa::class)
->disableOriginalConstructor()
->getMock();
Para cenários avançados, a biblioteca Mockery oferece suporte a métodos estáticos e final via shouldReceive, mas foge ao escopo nativo do PHPUnit.
6. Testando Dependências Externas (HTTP, API)
Mockar clientes HTTP é essencial para testes rápidos e confiáveis. Exemplo com Guzzle:
<?php
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
class ServicoCepTest extends TestCase
{
public function testConsultaCepRetornaEndereco(): void
{
// Mock do cliente HTTP
$httpClient = $this->createMock(ClientInterface::class);
// Simula resposta da API ViaCEP
$response = new Response(200, [], json_encode([
'cep' => '01001-000',
'logradouro' => 'Praça da Sé',
'bairro' => 'Sé',
'localidade' => 'São Paulo',
]));
$httpClient->method('request')
->with('GET', $this->stringContains('01001-000'))
->willReturn($response);
$servico = new ServicoCep($httpClient);
$endereco = $servico->consultar('01001-000');
$this->assertEquals('Praça da Sé', $endereco->logradouro);
}
}
Isso permite testar toda a lógica de parsing e tratamento de erros sem chamar o serviço real.
7. Boas Práticas e Armadilhas Comuns
Evite over-mocking
Se uma classe depende de objetos de valor ou coleções simples, use instâncias reais. Mockie apenas o que envolve I/O, estado global ou chamadas lentas.
Limpeza entre testes
O PHPUnit já reinicializa mocks entre métodos de teste, mas para objetos com estado compartilhado (ex.: singleton), use tearDown():
protected function tearDown(): void
{
\Mockery::close(); // se usar Mockery
parent::tearDown();
}
Teste de contratos
Sempre que possível, faça um teste de integração que verifique se seu mock reflete a interface real:
$this->assertInstanceOf(Interface::class, $mock); // verificação de tipo
Armadilha: with() sem especificar argumentos
Se você usa with(), todos os argumentos devem ser especificados. Para ignorar, use $this->anything().
8. Ferramentas Complementares e Próximos Passos
O PHPUnit nativo cobre 90% dos casos de mocking. No entanto, para cenários complexos, a biblioteca Mockery oferece:
- DSL mais expressiva (
shouldReceive,andReturn) - Suporte a métodos
finale estáticos - Matchers de argumentos mais flexíveis
Integração com data providers permite testar múltiplos cenários com o mesmo mock:
/**
* @dataProvider provedorCeps
*/
public function testCepValido(string $cep, bool $esperado): void
{
// mock configurado dinamicamente
}
No próximo artigo da série, exploraremos testes de integração com bancos de dados reais, usando fixtures e transações para garantir isolamento.
Lembrete final: mockie com moderação. O objetivo não é testar o mock, mas sim o comportamento da classe sob teste. Quando um teste fica complexo demais com mocks, considere refatorar o código ou usar um fake mais simples.
Referências
- PHPUnit Documentation: Test Doubles — Documentação oficial sobre stubs, mocks e dublês de teste no PHPUnit.
- Mockery: Simple Mocking for PHP — Biblioteca alternativa de mocking com suporte a métodos estáticos e sintaxe fluente.
- PHPUnit: Creating Test Stubs — Guia prático sobre
createStub()e configuração de retornos. - Guzzle Documentation: Testing — Como mockar clientes HTTP Guzzle para testes de API.
- Refactoring Guru: Test Doubles — Explicação visual e conceitual sobre os diferentes tipos de dublês de teste.
- PHP The Right Way: Testing — Seção sobre boas práticas de teste em PHP, incluindo mocking e isolamento.