Introdução: Por que testar em PHP não é uma opção, mas um padrão
No desenvolvimento PHP moderno, os testes há muito deixaram de ser "boa prática" e se tornaram uma etapa obrigatória em projetos de produção. Sem cobertura de código com testes, você corre o risco de lançar em produção um erro que pode custar dinheiro e reputação ao negócio. Neste artigo, vamos analisar três níveis de teste: unit (unitário), integration (integração) e e2e (ponta a ponta). Você aprenderá como eles diferem, quais ferramentas usar (PHPUnit, Pest, Laravel Dusk, Codeception) e como construir uma estratégia de teste adequada para seu projeto PHP.
1. Testes Unitários: testando unidades isoladas de código
Testes unitários verificam o funcionamento de classes, funções ou métodos individuais isolados de dependências externas (BD, API, sistema de arquivos). Este é o tipo de teste mais rápido e barato. A principal ferramenta em PHP é o PHPUnit (padrão de fato) e sua alternativa "moderna" Pest.
1.1. Instalação e primeiro teste com PHPUnit
Instale o PHPUnit via Composer:
composer require --dev phpunit/phpunitCrie um teste para uma classe calculadora simples:
<?php// src/Calculator.phpclass Calculator { public function add(int $a, int $b): int { return $a + $b; }}
// tests/CalculatorTest.phpuse PHPUnit\\Framework\\TestCase;
class CalculatorTest extends TestCase { public function testAddition(): void { $calc = new Calculator(); $result = $calc->add(2, 3); $this->assertEquals(5, $result); }}Execute o teste:
./vendor/bin/phpunit tests1.2. Uso de Objetos Mock
Para isolar a classe sendo testada, "substituímos" suas dependências usando mocks. Exemplo com um repositório de usuários:
<?phpclass UserService { private UserRepository $repo; public function __construct(UserRepository $repo) { $this->repo = $repo; } public function getUserName(int $id): string { $user = $this->repo->findById($id); return $user ? $user->getName() : 'Guest'; }}
// No teste$mockRepo = $this->createMock(UserRepository::class);$mockRepo->method('findById') ->with(1) ->willReturn(new User('Alice'));
$service = new UserService($mockRepo);$this->assertEquals('Alice', $service->getUserName(1));Best practice: Escreva testes unitários para toda a lógica de negócios. Eles devem ser executados em segundos e não depender do ambiente.
2. Testes de Integração: verificando a interação com BD, API e serviços externos
Testes de integração verificam como suas classes funcionam juntas com componentes reais: banco de dados, cache, serviços HTTP externos. Eles são mais lentos que os testes unitários, mas fornecem mais confiança de que o sistema funciona como um todo unificado.
2.1. Testando o trabalho com banco de dados (Laravel + RefreshDatabase)
Se você usa Laravel, os testes de integração se tornam muito simples graças ao trait RefreshDatabase:
<?phpnamespace Tests\\Feature;
use Tests\\TestCase;use Illuminate\\Foundation\\Testing\\RefreshDatabase;
class UserRegistrationTest extends TestCase { use RefreshDatabase;
public function test_user_can_register(): void { $response = $this->post('/register', [ 'name' => 'John', 'email' => 'john@example.com', 'password' => 'secret123', ]);
$response->assertStatus(302); // redirect after success $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); }}O trait RefreshDatabase migra automaticamente o BD antes dos testes e