Bem-vindo à aula sobre mocks e dublês em PHP! Nesta aula, você aprenderá a testar código que depende de outros objetos ou serviços, isolando essas dependências para tornar seus testes mais rápidos, confiáveis e focados. Dublês de teste são fundamentais no desenvolvimento orientado a testes (TDD) e em testes de unidade, pois permitem simular comportamentos complexos sem depender de implementações reais.

Vamos explorar os conceitos de stubs e mocks, como aplicá-los usando ferramentas como PHPUnit e Mockery, e como a injeção de dependências facilita a criação de código testável. Ao final, você terá uma compreensão sólida para escrever testes de alta qualidade em seus projetos PHP.

Stubs e mocks

Dublês de teste são objetos que substituem dependências reais durante os testes. Existem vários tipos, mas os mais comuns são stubs e mocks. Um stub é um objeto que retorna valores pré-definidos para chamadas de método, sem verificar se o método foi chamado ou com quais argumentos. Ele é usado para fornecer dados controlados ao código sob teste, garantindo que o teste não dependa de serviços externos (como bancos de dados, APIs, etc.).

Um mock é semelhante a um stub, mas além de retornar valores, ele verifica se determinados métodos foram chamados com os argumentos esperados. Isso permite testar o comportamento da unidade de código, não apenas o resultado. Em outras palavras, mocks são usados para verificar interações.

Vejamos um exemplo simples em PHP usando PHPUnit. Suponha que temos uma classe PedidoService que depende de um gateway de pagamento:

class PedidoService {
    private $gateway;

    public function __construct(GatewayPagamento $gateway) {
        $this->gateway = $gateway;
    }

    public function processar(Pedido $pedido): bool {
        return $this->gateway->cobrar($pedido->getValor(), $pedido->getCartao());
    }
}

Para testar PedidoService sem chamar o gateway real, podemos criar um stub que sempre retorna true:

$stub = $this->createStub(GatewayPagamento::class);
$stub->method('cobrar')->willReturn(true);

$service = new PedidoService($stub);
$resultado = $service->processar($pedido);

$this->assertTrue($resultado);

Já com um mock, podemos verificar se o método cobrar foi chamado com os argumentos corretos:

$mock = $this->createMock(GatewayPagamento::class);
$mock->expects($this->once())
     ->method('cobrar')
     ->with(100.0, '4111-1111-1111-1111')
     ->willReturn(true);

$service = new PedidoService($mock);
$resultado = $service->processar($pedido);

$this->assertTrue($resultado);

Note que o mock garante que o método foi chamado exatamente uma vez e com os argumentos esperados, o que é uma forma de testar o contrato entre as classes.

Testando dependências

Quando você testa uma classe que possui dependências, precisa decidir se vai usar as dependências reais ou dublês. Em testes de unidade, o objetivo é isolar a unidade de trabalho, portanto, usamos dublês para todas as dependências. Isso evita efeitos colaterais, como acesso a banco de dados ou rede, que podem tornar os testes lentos e instáveis.

Considere uma classe RelatorioService que usa um repositório para buscar dados:

class RelatorioService {
    private $repositorio;

    public function __construct(RepositorioInterface $repositorio) {
        $this->repositorio = $repositorio;
    }

    public function gerar(): array {
        $dados = $this->repositorio->buscarTodos();
        // Processa e retorna
        return $dados;
    }
}

No teste, criamos um stub do repositório que retorna um conjunto fixo de dados:

$stub = $this->createStub(RepositorioInterface::class);
$stub->method('buscarTodos')->willReturn([['id' => 1, 'nome' => 'Produto A']]);

$service = new RelatorioService($stub);
$resultado = $service->gerar();

$this->assertCount(1, $resultado);

Isso permite testar a lógica de gerar() sem depender do banco de dados. Se a dependência for uma interface, fica fácil criar dublês, pois não precisamos nos preocupar com implementações concretas.

Além disso, é importante testar como a classe interage com suas dependências: se ela chama os métodos corretos, na ordem correta, e com os argumentos certos. Mocks são úteis para isso, como vimos no exemplo anterior.

Injeção para testabilidade

Para que o uso de dublês seja viável, o código precisa ser projetado para permitir a injeção de dependências. Isso significa que as dependências devem ser passadas ao objeto via construtor, setters ou um container de injeção, em vez de serem criadas internamente. Isso é conhecido como Inversão de Controle (IoC) e é um princípio fundamental do design orientado a objetos.

Vamos comparar duas abordagens: uma sem injeção, que cria a dependência internamente, e outra com injeção.

Sem injeção:

class PedidoService {
    private $gateway;

    public function __construct() {
        $this->gateway = new GatewayPagamento(); // cria internamente
    }
}

Nesse caso, é impossível substituir o gateway por um dublê nos testes, pois a instância é criada dentro do construtor. Isso torna o teste difícil e acoplado à implementação real.

Com injeção:

class PedidoService {
    private $gateway;

    public function __construct(GatewayPagamento $gateway) {
        $this->gateway = $gateway;
    }
}

Agora, nos testes, podemos passar um stub ou mock. A injeção de dependências também traz outros benefícios, como maior flexibilidade e desacoplamento entre classes, facilitando manutenção e evolução do código.

Além do construtor, podemos usar métodos setter ou propriedades públicas, mas o construtor é a forma mais comum e garante que a dependência seja definida no momento da criação do objeto. Em frameworks modernos, como Laravel e Symfony, a injeção de dependências é gerenciada automaticamente por um container, mas em testes, você mesmo pode instanciar os objetos com dublês.

Boas práticas

Ao trabalhar com mocks e dublês, siga estas boas práticas para manter seus testes claros e eficazes:

  • Prefira stubs para fornecer dados, mocks para verificar interações. Use stubs quando você só precisa de um valor de retorno; use mocks quando precisar garantir que métodos foram chamados.
  • Evite criar mocks para classes que você não possui ou que são muito complexas. Prefira usar interfaces para definir contratos e facilitar a criação de dublês.
  • Não exagere nas verificações. Verifique apenas o que é essencial para o comportamento da unidade. Verificações excessivas tornam os testes frágeis e difíceis de manter.
  • Use nomes descritivos para os dublês. Em vez de $mock, use $gatewayMock para deixar claro qual dependência está sendo simulada.
  • Mantenha os testes independentes. Cada teste deve ser capaz de rodar isoladamente, sem depender de outros testes ou de estado global.
  • Considere usar bibliotecas como Mockery para uma API mais expressiva e recursos avançados, como mocks parciais e spies.

Além disso, lembre-se de que dublês não devem ser usados em testes de integração, onde o objetivo é verificar a interação entre componentes reais. Nesses casos, use bancos de dados de teste ou serviços reais em ambiente controlado.

Referências

Exercícios

  1. Crie um teste para a classe PedidoService usando um stub para o gateway de pagamento que retorna false e verifique que processar() retorna false.
  2. ✓ Resposta:
    $stub = $this->createStub(GatewayPagamento::class);
    $stub->method('cobrar')->willReturn(false);
    
    $service = new PedidoService($stub);
    $resultado = $service->processar($pedido);
    
    $this->assertFalse($resultado);
  3. Utilize um mock para verificar que o método enviar de uma classe Notificador é chamado exatamente uma vez com o argumento 'pedido-criado' ao executar o método criarPedido de um serviço.
  4. ✓ Resposta:
    $mock = $this->createMock(Notificador::class);
    $mock->expects($this->once())
         ->method('enviar')
         ->with('pedido-criado');
    
    $service = new PedidoService($mock);
    $service->criarPedido();
  5. Refatore a classe PedidoService para usar injeção de dependência via construtor, caso ainda não use. Escreva um teste que utilize um mock da dependência.
  6. ✓ Resposta:
    class PedidoService {
        private $gateway;
        public function __construct(GatewayPagamento $gateway) {
            $this->gateway = $gateway;
        }
    }
    
    // Teste:
    $mock = $this->createMock(GatewayPagamento::class);
    $mock->method('cobrar')->willReturn(true);
    $service = new PedidoService($mock);
  7. Explique a diferença entre stub e mock e dê um exemplo de cada, em PHP.
  8. ✓ Resposta: Um stub é usado para fornecer dados pré-definidos, sem verificar chamadas. Um mock verifica interações. Exemplo de stub:
    $stub = $this->createStub(Repositorio::class);
    $stub->method('buscar')->willReturn(['x']);
    Exemplo de mock:
    $mock = $this->createMock(Notificador::class);
    $mock->expects($this->once())->method('enviar');
  9. Escreva um teste para uma classe Calculadora que dependa de um serviço de log. Use um mock para verificar que o log é chamado com a mensagem 'Resultado: 5' quando a operação soma é executada com 2 e 3.
  10. ✓ Resposta:
    $mockLog = $this->createMock(LoggerInterface::class);
    $mockLog->expects($this->once())
            ->method('info')
            ->with('Resultado: 5');
    
    $calc = new Calculadora($mockLog);
    $calc->somar(2, 3);

Observações finais

Dominar o uso de dublês de teste é essencial para escrever testes de unidade eficazes. Lembre-se de que o objetivo é isolar a unidade de código e verificar seu comportamento de forma previsível. Use stubs para simplificar e mocks para verificar interações, sempre com moderação. A injeção de dependências é um pré-requisito para isso, então projete suas classes com interfaces e dependências explícitas. Com essas práticas, seus testes serão mais rápidos, estáveis e fáceis de manter.