Testes com PHPUnit: fundamentos

1. Introdução ao PHPUnit e configuração do ambiente

PHPUnit é o framework de testes unitários mais utilizado no ecossistema PHP. Criado por Sebastian Bergmann, ele permite que desenvolvedores escrevam testes automatizados para verificar se pequenas unidades de código (métodos e funções) funcionam conforme o esperado. Testar seu código PHP traz benefícios como detecção precoce de bugs, segurança para refatorações e documentação viva do comportamento esperado do sistema.

Para instalar o PHPUnit, utilize o Composer, o gerenciador de dependências do PHP:

composer require --dev phpunit/phpunit

Após a instalação, organize seu projeto com a seguinte estrutura de diretórios:

meu-projeto/
├── src/
│   └── Calculadora.php
├── tests/
│   └── CalculadoraTest.php
├── vendor/
├── composer.json
└── phpunit.xml

No arquivo composer.json, configure o autoloading para mapear o namespace App para a pasta src/:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    }
}

Execute composer dump-autoload para gerar o autoloading. Agora você pode rodar seu primeiro teste:

vendor/bin/phpunit

2. Escrevendo casos de teste básicos

Vamos criar uma classe Calculadora simples e seus respectivos testes. Primeiro, a classe a ser testada em src/Calculadora.php:

<?php

namespace App;

class Calculadora
{
    public function somar(float $a, float $b): float
    {
        return $a + $b;
    }

    public function dividir(float $a, float $b): float
    {
        if ($b === 0.0) {
            throw new \InvalidArgumentException('Divisão por zero não é permitida');
        }
        return $a / $b;
    }
}

Agora, o caso de teste em tests/CalculadoraTest.php:

<?php

namespace Tests;

use App\Calculadora;
use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    public function testSomar()
    {
        $calc = new Calculadora();
        $resultado = $calc->somar(2, 3);
        $this->assertEquals(5, $resultado);
    }

    /** @test */
    public function soma_com_numeros_negativos()
    {
        $calc = new Calculadora();
        $this->assertEquals(-1, $calc->somar(2, -3));
    }

    public function testSomarComZero()
    {
        $calc = new Calculadora();
        $this->assertEquals(5, $calc->somar(5, 0));
    }
}

Observe que podemos usar o prefixo test ou a anotação @test para identificar métodos de teste. As asserções fundamentais incluem:

  • assertEquals($expected, $actual) — verifica igualdade
  • assertTrue($condition) — verifica se é verdadeiro
  • assertFalse($condition) — verifica se é falso
  • assertNull($variable) — verifica se é nulo

3. Organização e boas práticas com fixtures

Para evitar repetição de código, utilize os métodos setUp() e tearDown():

<?php

namespace Tests;

use App\Calculadora;
use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    private Calculadora $calculadora;

    protected function setUp(): void
    {
        $this->calculadora = new Calculadora();
    }

    protected function tearDown(): void
    {
        // Limpeza de recursos, se necessário
        unset($this->calculadora);
    }

    public function testSomar()
    {
        $this->assertEquals(5, $this->calculadora->somar(2, 3));
    }

    public function testDividir()
    {
        $this->assertEquals(2, $this->calculadora->dividir(10, 5));
    }
}

Cada teste deve ser independente — a ordem de execução não deve importar. Evite compartilhar estado entre testes.

4. Testando exceções e erros esperados

Para verificar se uma exceção é lançada corretamente, use expectException():

public function testDivisaoPorZeroLancaExcecao()
{
    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Divisão por zero não é permitida');
    $this->expectExceptionCode(0);

    $this->calculadora->dividir(10, 0);
}

Outro exemplo com validação de dados:

public function testEmailInvalidoLancaExcecao()
{
    $validador = new Validador();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Email inválido');

    $validador->validarEmail('email-invalido');
}

5. Data providers para testes parametrizados

Data providers eliminam repetição ao testar múltiplos cenários:

<?php

namespace Tests;

use App\Calculadora;
use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    private Calculadora $calculadora;

    protected function setUp(): void
    {
        $this->calculadora = new Calculadora();
    }

    /** @dataProvider somarDataProvider */
    public function testSomarComDataProvider($a, $b, $esperado)
    {
        $this->assertEquals($esperado, $this->calculadora->somar($a, $b));
    }

    public static function somarDataProvider(): array
    {
        return [
            'números positivos' => [2, 3, 5],
            'números negativos' => [-1, -1, -2],
            'com zero' => [5, 0, 5],
            'decimais' => [1.5, 2.5, 4.0],
        ];
    }
}

Cada sub-array contém os argumentos para o método de teste. As chaves descritivas ajudam na identificação de falhas.

6. Testando código que depende de recursos externos

Quando uma classe depende de serviços externos (banco de dados, APIs), use stubs e mocks para isolar o teste:

<?php

namespace Tests;

use App\UserService;
use App\UserRepository;
use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
    public function testBuscarUsuarioPorId()
    {
        // Cria um stub do repositório
        $stub = $this->createStub(UserRepository::class);

        // Configura o retorno esperado
        $stub->method('findById')
             ->with(1)
             ->willReturn(['id' => 1, 'nome' => 'João']);

        $service = new UserService($stub);
        $usuario = $service->buscar(1);

        $this->assertEquals('João', $usuario['nome']);
    }

    public function testSalvarUsuarioComMock()
    {
        $mock = $this->createMock(UserRepository::class);

        $mock->expects($this->once())
             ->method('save')
             ->with($this->callback(function($usuario) {
                 return $usuario['nome'] === 'Maria';
             }));

        $service = new UserService($mock);
        $service->salvar(['nome' => 'Maria']);
    }
}

7. Asserções avançadas e análise de cobertura

PHPUnit oferece asserções especializadas para arrays e objetos:

public function testAssercoesAvancadas()
{
    $dados = ['nome' => 'Ana', 'idade' => 30, 'tags' => ['php', 'testes']];

    $this->assertCount(3, $dados);
    $this->assertContains('php', $dados['tags']);
    $this->assertArrayHasKey('nome', $dados);
    $this->assertIsArray($dados['tags']);

    $usuario = new \stdClass();
    $usuario->nome = 'Carlos';
    $this->assertInstanceOf(\stdClass::class, $usuario);
    $this->assertObjectHasProperty('nome', $usuario);
}

Para gerar relatório de cobertura, execute:

vendor/bin/phpunit --coverage-html coverage

Isso cria uma pasta coverage/ com relatórios detalhados mostrando quais linhas, branches e métodos foram executados durante os testes.

8. Integração contínua e boas práticas finais

Configure a execução automática de testes com GitHub Actions. Crie .github/workflows/phpunit.yml:

name: PHPUnit Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'

    - name: Install dependencies
      run: composer install --prefer-dist

    - name: Run tests
      run: vendor/bin/phpunit

Personalize a execução com phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         cacheDirectory=".phpunit.cache">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory>src</directory>
        </include>
    </coverage>
</phpunit>

Checklist de qualidade:
- ✅ Testes rápidos (segundos, não minutos)
- ✅ Isolados (sem dependência entre si)
- ✅ Legíveis (nomes descritivos e data providers)
- ✅ Cobertura significativa (priorize lógica de negócio)
- ✅ Automatizados na CI

Lembre-se: testes não são opcionais — são investimento na qualidade e manutenibilidade do seu código PHP.

Referências