O PHPUnit é o framework de testes unitários mais utilizado no ecossistema PHP. Ele fornece uma estrutura robusta para escrever e executar testes automatizados, garantindo que seu código funcione como esperado e facilitando a manutenção ao longo do tempo. Nesta aula, vamos explorar os principais recursos do PHPUnit, desde as asserções básicas até técnicas mais avançadas como data providers e organização de testes.

Dominar o PHPUnit é essencial para qualquer desenvolvedor PHP profissional, pois testes automatizados são parte fundamental de um fluxo de desenvolvimento moderno, especialmente em projetos com integração contínua. Vamos mergulhar nos detalhes práticos, com exemplos prontos para uso.

Asserções

As asserções são o coração de qualquer teste unitário: elas verificam se o resultado obtido é igual ao esperado. O PHPUnit oferece uma vasta gama de métodos de asserção, herdados da classe PHPUnit\Framework\TestCase. Cada asserção compara um valor real com o esperado e, se a condição não for satisfeita, o teste falha.

Algumas das asserções mais comuns incluem assertEquals, que verifica igualdade (usando comparação fraca), assertSame, que verifica identidade (mesmo valor e tipo), e assertTrue/assertFalse para booleanos. Também temos assertNull, assertNotNull, assertCount para arrays, entre muitas outras. A escolha da asserção correta é crucial para testes precisos.

use PHPUnit\Framework\TestCase;

class AssertsTest extends TestCase
{
    public function testEquality()
    {
        $result = 1 + 1;
        $this->assertEquals(2, $result); // passa
        $this->assertSame(2, $result);   // passa
    }

    public function testBoolean()
    {
        $this->assertTrue(10 > 5);
        $this->assertFalse(10 < 5);
    }

    public function testNull()
    {
        $value = null;
        $this->assertNull($value);
        $this->assertNotNull('foo');
    }

    public function testArrayCount()
    {
        $array = [1, 2, 3];
        $this->assertCount(3, $array);
    }
}

Além dessas, o PHPUnit fornece asserções para exceções, como expectException, e para strings, como assertStringContainsString. É importante consultar a documentação oficial para conhecer todas as opções e usá-las adequadamente.

Setup e teardown

Em testes unitários, é comum precisar preparar o ambiente antes de cada teste e limpá-lo depois. O PHPUnit oferece métodos especiais: setUp() é executado antes de cada método de teste, e tearDown() é executado após cada método. Isso garante que cada teste seja independente e não sofra interferência de testes anteriores.

Além disso, existem métodos estáticos setUpBeforeClass() e tearDownAfterClass(), que rodam uma única vez antes da primeira execução e depois da última, respectivamente. Eles são úteis para inicializar recursos caros, como conexões com banco de dados.

use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    private $connection;

    public static function setUpBeforeClass(): void
    {
        // Inicializa conexão única
        // self::$connection = new PDO(...);
    }

    protected function setUp(): void
    {
        // Cria um novo registro de teste
        // $this->connection->exec('INSERT INTO ...');
    }

    protected function tearDown(): void
    {
        // Remove o registro de teste
        // $this->connection->exec('DELETE FROM ...');
    }

    public static function tearDownAfterClass(): void
    {
        // Fecha a conexão
        // self::$connection = null;
    }

    public function testSomething()
    {
        // Teste que depende do setUp
        $this->assertTrue(true);
    }
}

Usar setUp() e tearDown() é uma boa prática para manter os testes isolados e legíveis. No entanto, evite fazer trabalho pesado nesses métodos, pois eles são chamados para cada teste, o que pode tornar a suíte lenta.

Data providers

Data providers são métodos que fornecem conjuntos de dados para um teste, permitindo que o mesmo teste seja executado múltiplas vezes com diferentes argumentos e resultados esperados. Isso reduz a duplicação de código e torna os testes mais abrangentes.

Para usar um data provider, você adiciona a anotação @dataProvider nomeDoMetodo no docblock do método de teste. O método do provider deve retornar um array de arrays, onde cada sub-array é um conjunto de argumentos que serão passados ao teste.

use PHPUnit\Framework\TestCase;

class DataProviderTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public static function additionProvider(): array
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 2],
            [2, 2, 4],
        ];
    }
}

Data providers são especialmente úteis para testar funções matemáticas, validações, conversões e qualquer lógica que tenha múltiplos casos de entrada. Eles também podem ser nomeados para gerar mensagens de erro mais claras, usando chaves associativas no retorno do provider.

Organização

A organização dos testes é fundamental para a manutenibilidade do projeto. O PHPUnit segue convenções que ajudam a estruturar a suíte de testes: normalmente, cada classe de teste corresponde a uma classe do código de produção, e os métodos de teste são nomeados com prefixo test ou com a anotação @test.

É recomendado colocar os testes em um diretório separado, como tests, e usar o composer.json para autoload. O arquivo de configuração phpunit.xml permite definir o diretório de testes, filtros, grupos e outras opções. A organização em grupos é útil para executar apenas um subconjunto de testes, como testes de integração ou testes lentos.

// tests/CalculatorTest.php
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAddition()
    {
        $calc = new Calculator();
        $this->assertEquals(4, $calc->add(2, 2));
    }
}

// phpunit.xml (exemplo)
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

Além disso, o uso de namespaces e PSR-4 ajuda a mapear as classes de teste para as classes de produção. Manter uma estrutura consistente e legível torna mais fácil para outros desenvolvedores entenderem e contribuírem com os testes.

Boas práticas

Algumas boas práticas ao trabalhar com PHPUnit: mantenha os testes pequenos e focados em uma única unidade; use nomes descritivos para os métodos de teste; evite dependências entre testes; e sempre verifique a cobertura de código para identificar áreas não testadas. Além disso, é importante integrar os testes ao seu fluxo de CI/CD para que sejam executados automaticamente a cada alteração.

Exercícios

  1. Crie uma classe Math com um método estático multiply($a, $b) que retorna o produto. Escreva um teste PHPUnit que verifique a multiplicação de dois números positivos e de um número negativo.

✓ Resposta:
class Math {
    public static function multiply($a, $b) {
        return $a * $b;
    }
}

use PHPUnit\Framework\TestCase;

class MathTest extends TestCase {
    public function testMultiply() {
        $this->assertEquals(6, Math::multiply(2, 3));
        $this->assertEquals(-6, Math::multiply(2, -3));
    }
}
  1. Utilizando o método setUp(), crie um teste que inicializa um array vazio e, em cada teste, adiciona um elemento e verifica o tamanho. Escreva o código completo.

✓ Resposta:
use PHPUnit\Framework\TestCase;

class ArrayTest extends TestCase {
    private $array;

    protected function setUp(): void {
        $this->array = [];
    }

    public function testAddOne() {
        $this->array[] = 'item';
        $this->assertCount(1, $this->array);
    }

    public function testAddTwo() {
        $this->array[] = 'item1';
        $this->array[] = 'item2';
        $this->assertCount(2, $this->array);
    }
}
  1. Crie um data provider para testar uma função isEven($num) que retorna true se o número for par. O provider deve incluir números pares e ímpares.

✓ Resposta:
function isEven($num) {
    return $num % 2 === 0;
}

use PHPUnit\Framework\TestCase;

class IsEvenTest extends TestCase {
    /**
     * @dataProvider evenProvider
     */
    public function testIsEven($num, $expected) {
        $this->assertSame($expected, isEven($num));
    }

    public static function evenProvider(): array {
        return [
            [2, true],
            [3, false],
            [0, true],
            [-1, false],
            [10, true],
        ];
    }
}
  1. Escreva um teste que verifica se uma exceção é lançada quando uma função divide($a, $b) recebe 0 como divisor. Use a asserção expectException.

✓ Resposta:
function divide($a, $b) {
    if ($b == 0) {
        throw new InvalidArgumentException('Divisão por zero');
    }
    return $a / $b;
}

use PHPUnit\Framework\TestCase;

class DivideTest extends TestCase {
    public function testDivideByZero() {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Divisão por zero');
        divide(10, 0);
    }
}
  1. Organize os testes do exercício 1 em um diretório tests e configure um phpunit.xml para executá-los. Escreva o conteúdo do arquivo de configuração e o comando para rodar os testes.

✓ Resposta:
// phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

// Comando: ./vendor/bin/phpunit

Referências