Nesta aula, vamos mergulhar no PHPUnit, o framework de testes mais utilizado no ecossistema PHP. Você já deve ter uma noção básica de testes, mas aqui vamos aprofundar em recursos essenciais para escrever testes robustos e bem organizados. Vamos explorar as principais ferramentas que o PHPUnit oferece: asserções, métodos de setup e teardown, data providers e boas práticas de organização de testes.

Dominar esses conceitos é fundamental para garantir a qualidade do seu código, facilitar a manutenção e evitar regressões. Com o PHPUnit, você pode automatizar testes de forma integrada ao seu fluxo de desenvolvimento, seja em projetos pequenos ou grandes. Vamos começar!

Asserções

Asserções são o coração de qualquer teste: elas verificam se o resultado obtido é o esperado. O PHPUnit fornece uma vasta gama de métodos de asserção, herdados da classe PHPUnit\Framework\TestCase. Cada asserção compara um valor real com um valor esperado e, se a comparação falhar, o teste é marcado como falho, exibindo uma mensagem descritiva.

Além das asserções básicas como assertEquals e assertTrue, existem variações para tipos específicos, como assertSame (que também verifica o tipo), assertNull, assertInstanceOf, entre muitas outras. É importante escolher a asserção mais adequada para cada situação, pois isso torna os testes mais expressivos e fáceis de diagnosticar.

Vejamos um exemplo prático de várias asserções em um teste de uma classe simples de calculadora:

<?php
use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    public function testSoma()
    {
        $calc = new Calculadora();
        $this->assertEquals(4, $calc->soma(2, 2));
    }

    public function testSomaComFloat()
    {
        $calc = new Calculadora();
        $this->assertEqualsWithDelta(0.3, $calc->soma(0.1, 0.2), 0.0001);
    }

    public function testDivisaoPorZeroLancaExcecao()
    {
        $this->expectException(\InvalidArgumentException::class);
        $calc = new Calculadora();
        $calc->divide(10, 0);
    }
}

No exemplo acima, usamos assertEquals para comparar valores, assertEqualsWithDelta para lidar com imprecisões de ponto flutuante, e expectException para verificar que uma exceção é lançada. O PHPUnit oferece mais de 100 métodos de asserção; consulte a documentação oficial para explorar todos.

Setup e teardown

Em muitos testes, precisamos preparar um ambiente comum antes de cada teste e limpá-lo após a execução. O PHPUnit fornece métodos especiais: setUp() e tearDown(), que são executados antes e depois de cada método de teste, respectivamente. Isso evita repetição de código e garante que cada teste seja independente.

Além desses, existem setUpBeforeClass() e tearDownAfterClass(), que são executados uma única vez antes da primeira execução de teste da classe e após a última, respectivamente. São úteis para inicializar recursos caros, como conexões de banco de dados.

Vamos ver um exemplo típico de uso de setUp():

<?php
use PHPUnit\Framework\TestCase;

class UsuarioTest extends TestCase
{
    private $usuario;

    protected function setUp(): void
    {
        $this->usuario = new Usuario('João', 'joao@example.com');
    }

    protected function tearDown(): void
    {
        $this->usuario = null;
    }

    public function testNome()
    {
        $this->assertEquals('João', $this->usuario->getNome());
    }

    public function testEmail()
    {
        $this->assertEquals('joao@example.com', $this->usuario->getEmail());
    }
}

Nesse exemplo, setUp() cria uma nova instância de Usuario antes de cada teste, garantindo que os testes não interfiram entre si. O tearDown() é usado para limpar recursos, embora muitas vezes não seja necessário se os objetos forem descartados automaticamente.

É importante lembrar que o setUp() é chamado antes de cada método de teste, então qualquer estado modificado em um teste não afeta o próximo. Isso promove isolamento e confiabilidade.

Data providers

Data providers são métodos que fornecem conjuntos de dados para testes, permitindo executar o mesmo teste com múltiplas entradas. Isso é extremamente útil para testar funções com muitas variações, evitando escrever vários métodos de teste quase idênticos.

Para usar um data provider, basta adicionar a anotação @dataProvider nomeDoMetodo no docblock do método de teste. O método do data provider deve retornar um array de arrays, onde cada sub-array contém os argumentos para a execução do teste.

Vamos criar um exemplo com uma função que valida se um número é par:

<?php
use PHPUnit\Framework\TestCase;

class ParTest extends TestCase
{
    /**
     * @dataProvider parProvider
     */
    public function testEhPar($numero, $esperado)
    {
        $this->assertEquals($esperado, ehPar($numero));
    }

    public function parProvider(): array
    {
        return [
            [2, true],
            [3, false],
            [10, true],
            [7, false],
        ];
    }
}

Quando o teste é executado, o PHPUnit itera sobre os dados fornecidos e executa o teste uma vez para cada conjunto. Isso facilita a cobertura de muitos casos sem duplicação de código.

Além disso, data providers podem fornecer dados nomeados, usando chaves associativas, o que ajuda a identificar qual caso falhou. Por exemplo:

public function parProvider(): array
{
    return [
        'dois é par' => [2, true],
        'três é ímpar' => [3, false],
    ];
}

Isso torna as mensagens de erro mais claras, pois o nome do caso é exibido.

Organização

Uma boa organização dos testes é crucial para a manutenibilidade do projeto. O PHPUnit permite estruturar seus testes de várias maneiras, desde simples arquivos até suítes de testes complexas.

Em projetos PHP, é comum colocar os testes em um diretório tests na raiz, espelhando a estrutura do código-fonte. Por exemplo, se você tem uma classe App\Services\UserService, o teste correspondente seria tests/App/Services/UserServiceTest.php.

Além disso, você pode criar suítes de testes no arquivo phpunit.xml, especificando quais diretórios ou arquivos devem ser executados. Isso permite rodar apenas uma parte dos testes, por exemplo, testes unitários e testes de integração separadamente.

Outra prática recomendada é usar namespaces nos testes para evitar conflitos e facilitar o autoload. O Composer pode ser configurado para carregar as classes de teste com o autoload-dev.

Exemplo de estrutura de diretórios:

projeto/
├── src/
│   └── App/
│       └── Services/
│           └── UserService.php
├── tests/
│   └── App/
│       └── Services/
│           └── UserServiceTest.php
└── phpunit.xml

No phpunit.xml, você pode configurar a suíte padrão:

<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>
</phpunit>

Essa organização ajuda a manter os testes claros e separados por tipo, facilitando a execução seletiva.

Boas práticas

Ao escrever testes com PHPUnit, algumas práticas podem melhorar a qualidade e a eficiência:

  • Testes independentes: cada teste deve ser autossuficiente e não depender de outros.
  • Nomes descritivos: use nomes de métodos que descrevam o comportamento esperado.
  • Assertivas específicas: use a asserção mais específica possível para o que está testando.
  • Evite lógica complexa nos testes: mantenha os testes simples, sem loops ou condicionais desnecessários.
  • Use data providers para variações: evite duplicação de código.
  • Mantenha os testes rápidos: testes lentos desencorajam a execução frequente.

Referências

Exercícios

  1. Crie uma classe Calculadora com métodos soma, subtracao, multiplicacao e divisao. Escreva testes para cada método usando asserções apropriadas.

    ✓ Resposta:
    <?php
    use PHPUnit\Framework\TestCase;
    
    class CalculadoraTest extends TestCase
    {
        public function testSoma()
        {
            $calc = new Calculadora();
            $this->assertEquals(4, $calc->soma(2, 2));
        }
    
        public function testSubtracao()
        {
            $calc = new Calculadora();
            $this->assertEquals(1, $calc->subtracao(3, 2));
        }
    
        public function testMultiplicacao()
        {
            $calc = new Calculadora();
            $this->assertEquals(6, $calc->multiplicacao(2, 3));
        }
    
        public function testDivisao()
        {
            $calc = new Calculadora();
            $this->assertEquals(2, $calc->divisao(6, 3));
        }
    }
    
  2. Utilize setUp() para inicializar uma instância de Calculadora e reescreva os testes do exercício 1, removendo a criação repetida.

    ✓ Resposta:
    <?php
    use PHPUnit\Framework\TestCase;
    
    class CalculadoraTest extends TestCase
    {
        private $calc;
    
        protected function setUp(): void
        {
            $this->calc = new Calculadora();
        }
    
        public function testSoma()
        {
            $this->assertEquals(4, $this->calc->soma(2, 2));
        }
    
        public function testSubtracao()
        {
            $this->assertEquals(1, $this->calc->subtracao(3, 2));
        }
    
        public function testMultiplicacao()
        {
            $this->assertEquals(6, $this->calc->multiplicacao(2, 3));
        }
    
        public function testDivisao()
        {
            $this->assertEquals(2, $this->calc->divisao(6, 3));
        }
    }
    
  3. Crie um data provider para testar a função ehPar com os números 2, 3, 10, 7, e 0.

    ✓ Resposta:
    <?php
    use PHPUnit\Framework\TestCase;
    
    class ParTest extends TestCase
    {
        /**
         * @dataProvider parProvider
         */
        public function testEhPar($numero, $esperado)
        {
            $this->assertEquals($esperado, ehPar($numero));
        }
    
        public function parProvider(): array
        {
            return [
                [2, true],
                [3, false],
                [10, true],
                [7, false],
                [0, true],
            ];
        }
    }
    
  4. Explique a diferença entre setUp() e setUpBeforeClass(). Dê um exemplo de quando usar cada um.

    ✓ Resposta:

    setUp() é executado antes de cada método de teste, garantindo que cada teste comece com um estado limpo e isolado. É útil para inicializar objetos que serão usados em todos os testes, como mocks ou instâncias de classes.

    setUpBeforeClass() é executado uma única vez antes do primeiro teste da classe. É útil para inicializar recursos caros ou estáticos que não precisam ser recriados a cada teste, como conexões de banco de dados ou configurações de ambiente.

    Exemplo: se você tem uma classe que testa um repositório de banco de dados, pode usar setUpBeforeClass() para abrir uma conexão única, e setUp() para criar dados de teste em cada teste.

  5. Organize os testes de um projeto com estrutura de diretórios src e tests no arquivo phpunit.xml, definindo duas suítes: uma para testes unitários e outra para testes de integração.

    ✓ Resposta:
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="unit">
                <directory>tests/Unit</directory>
            </testsuite>
            <testsuite name="integration">
                <directory>tests/Integration</directory>
            </testsuite>
        </testsuites>
    </phpunit>
    

    Estrutura de diretórios:

    src/
    tests/
      Unit/
      Integration/