Como escrever testes unitários eficazes em PHP
1. Fundamentos dos testes unitários em PHP
Testes unitários são a base da qualidade de software no ecossistema PHP. Eles verificam o comportamento de unidades individuais de código — geralmente métodos de classes — de forma isolada e repetível. No contexto da série Temas — Lista Final (1200 temas), dominar essa prática é essencial para qualquer desenvolvedor que deseja entregar código confiável e sustentável.
A principal diferença entre os tipos de teste é clara:
- Testes unitários: verificam uma única unidade de código, sem dependências externas (banco de dados, APIs, sistema de arquivos)
- Testes de integração: validam a interação entre múltiplas unidades ou com sistemas externos
- Testes funcionais: simulam o fluxo completo de uma funcionalidade do início ao fim
O PHPUnit é o framework padrão da indústria PHP, mantido por Sebastian Bergmann e amplamente adotado por frameworks como Laravel, Symfony e WordPress.
2. Configuração inicial do ambiente de testes
A instalação via Composer é simples:
composer require --dev phpunit/phpunit
A estrutura de diretórios recomendada segue o padrão PSR-4:
projeto/
├── src/
│ └── App/
│ └── Calculator.php
├── tests/
│ ├── Unit/
│ │ └── CalculatorTest.php
│ └── Feature/
└── phpunit.xml.dist
O arquivo de configuração phpunit.xml.dist permite personalizar a execução:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
verbose="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
</phpunit>
3. Escrevendo o primeiro teste unitário eficaz
A anatomia de uma classe de teste segue um padrão consistente. Suponha que temos uma calculadora simples em src/App/Calculator.php:
<?php
namespace App;
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new \InvalidArgumentException('Division by zero');
}
return $a / $b;
}
}
O teste correspondente em tests/Unit/CalculatorTest.php:
<?php
namespace Tests\Unit;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAddReturnsCorrectSum(): void
{
$result = $this->calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testAddWithNegativeNumbers(): void
{
$result = $this->calculator->add(-1, 5);
$this->assertEquals(4, $result);
}
public function testDivideByZeroThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Division by zero');
$this->calculator->divide(10, 0);
}
}
A nomenclatura clara dos métodos (usando test prefix ou o atributo #[Test] do PHPUnit 10+) e o uso de assertions específicas (assertEquals, assertTrue, assertCount, assertInstanceOf) tornam os testes legíveis e auto-documentados.
4. Isolamento e mocks com PHPUnit
Isolar dependências é crucial para testes unitários verdadeiros. Considere uma classe que depende de um serviço externo:
<?php
namespace App;
class OrderProcessor
{
public function __construct(
private PaymentGateway $gateway
) {}
public function process(Order $order): bool
{
if ($order->getTotal() <= 0) {
throw new \InvalidArgumentException('Invalid order total');
}
return $this->gateway->charge($order->getTotal());
}
}
O teste usa mocks para isolar o PaymentGateway:
<?php
namespace Tests\Unit;
use App\Order;
use App\OrderProcessor;
use App\PaymentGateway;
use PHPUnit\Framework\TestCase;
class OrderProcessorTest extends TestCase
{
public function testProcessChargesCorrectAmount(): void
{
$order = $this->createMock(Order::class);
$order->method('getTotal')->willReturn(100.0);
$gateway = $this->createMock(PaymentGateway::class);
$gateway->expects($this->once())
->method('charge')
->with(100.0)
->willReturn(true);
$processor = new OrderProcessor($gateway);
$result = $processor->process($order);
$this->assertTrue($result);
}
public function testProcessThrowsExceptionForInvalidOrder(): void
{
$this->expectException(\InvalidArgumentException::class);
$order = $this->createMock(Order::class);
$order->method('getTotal')->willReturn(0);
$gateway = $this->createMock(PaymentGateway::class);
$processor = new OrderProcessor($gateway);
$processor->process($order);
}
}
O uso de expects, willReturn e with permite verificar interações específicas, garantindo que o método charge seja chamado exatamente uma vez com o valor correto.
5. Dados de teste e fixtures inteligentes
Data providers eliminam duplicação e testam múltiplos cenários:
<?php
namespace Tests\Unit;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorDataProviderTest extends TestCase
{
/** @dataProvider additionProvider */
public function testAddWithMultipleScenarios(float $a, float $b, float $expected): void
{
$calculator = new Calculator();
$result = $calculator->add($a, $b);
$this->assertEquals($expected, $result);
}
public static function additionProvider(): array
{
return [
'positive numbers' => [2, 3, 5],
'negative numbers' => [-1, -2, -3],
'mixed signs' => [10, -5, 5],
'decimal numbers' => [0.1, 0.2, 0.3],
'zero' => [0, 0, 0],
];
}
}
Fixtures reutilizáveis no setUp() e o uso de traits evitam repetição. É fundamental evitar dependências entre testes — cada teste deve ser independente e executável em qualquer ordem.
6. Cobertura de código e métricas
Para gerar relatórios de cobertura, instale o Xdebug ou PCOV:
composer require --dev pcov/clobber
Execute com cobertura:
vendor/bin/phpunit --coverage-html coverage
As métricas importantes incluem:
- Cobertura de linhas: percentual de linhas executadas
- Cobertura de branches: percentual de caminhos condicionais testados
- Cobertura de métodos: percentual de métodos chamados durante os testes
Metas realistas giram em torno de 70-80% de cobertura de linhas. Valores próximos de 100% podem indicar testes frágeis ou foco excessivo em código trivial (getters/setters).
7. Boas práticas e padrões em testes PHP
A regra de ouro: teste comportamento, não implementação. Um teste que verifica detalhes internos (como chamadas privadas ou ordem específica de execução) quebra quando o código é refatorado, mesmo que o comportamento permaneça correto.
Evite testes frágeis:
- Não teste métodos privados diretamente — teste-os através de métodos públicos
- Não dependa de ordem de execução entre testes
- Use mocks para dependências lentas ou instáveis
Testes bem escritos permitem refatoração segura. Se você precisa alterar a implementação interna de uma classe, mas os testes continuam passando, você tem confiança de que não quebrou nada.
8. Integração contínua e automação
No GitHub Actions, um workflow básico para testes PHP:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: pcov
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run tests
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Check coverage
run: |
COVERAGE=$(php -r "echo (float)simplexml_load_file('coverage.xml')['coverage'];")
if (( $(echo "$COVERAGE < 70" | bc -l) )); then
echo "Coverage $COVERAGE% is below 70% threshold"
exit 1
fi
O cache de dependências e a paralelização de testes aceleram a execução em projetos maiores. Bloquear merges quando a cobertura cai abaixo do limite estabelecido mantém a qualidade do código ao longo do tempo.
Escrever testes unitários eficazes em PHP não é apenas uma prática técnica — é um investimento na saúde do projeto a longo prazo. Com PHPUnit, mocks inteligentes, data providers e integração contínua, você constrói uma base sólida que permite evolução segura e confiável do código.
Referências
- PHPUnit Documentation — Documentação oficial completa do PHPUnit, incluindo guias de instalação, configuração e todos os tipos de assertions disponíveis
- PHP: The Right Way - Testing — Seção sobre testes no guia de boas práticas PHP, com recomendações de ferramentas e padrões
- Mocking with PHPUnit — Guia oficial sobre criação de stubs, mocks e verificação de interações com PHPUnit
- Code Coverage Analysis — Documentação oficial sobre como configurar e interpretar relatórios de cobertura de código no PHPUnit
- GitHub Actions for PHP — Action oficial para configurar PHP no GitHub Actions, com suporte a extensões e ferramentas de cobertura
- Test-Driven Development with PHPUnit — Tutorial prático sobre TDD aplicado com PHPUnit, abordando ciclo red-green-refactor
- PHPUnit Best Practices — Artigo da PHP Architect com dicas avançadas para escrever testes mais limpos e eficientes