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 final e 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