Mocks e dublês
Esta aula aborda o conceito de dublês de teste em PHP, focando em stubs e mocks, como testar dependências, e a importância da injeção de dependências para a testabilidade. Além disso, apresenta boas práticas para criar testes eficazes e confiáveis.
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$gatewayMockpara 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
- PHPUnit Documentation - Test Doubles
- Mockery Documentation
- Mocks Aren't Stubs - Martin Fowler
- PSR-11: Container Interface
- Dependency Injection - Wikipedia
- PHPUnit Documentation
Exercícios
- Crie um teste para a classe
PedidoServiceusando um stub para o gateway de pagamento que retornafalsee verifique queprocessar()retornafalse. - Utilize um mock para verificar que o método
enviarde uma classeNotificadoré chamado exatamente uma vez com o argumento 'pedido-criado' ao executar o métodocriarPedidode um serviço. - Refatore a classe
PedidoServicepara usar injeção de dependência via construtor, caso ainda não use. Escreva um teste que utilize um mock da dependência. - Explique a diferença entre stub e mock e dê um exemplo de cada, em PHP.
- Escreva um teste para uma classe
Calculadoraque 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.
$stub = $this->createStub(GatewayPagamento::class);
$stub->method('cobrar')->willReturn(false);
$service = new PedidoService($stub);
$resultado = $service->processar($pedido);
$this->assertFalse($resultado);$mock = $this->createMock(Notificador::class);
$mock->expects($this->once())
->method('enviar')
->with('pedido-criado');
$service = new PedidoService($mock);
$service->criarPedido();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);$stub = $this->createStub(Repositorio::class);
$stub->method('buscar')->willReturn(['x']); Exemplo de mock: $mock = $this->createMock(Notificador::class);
$mock->expects($this->once())->method('enviar');$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.