Data providers e testes parametrizados

1. Introdução aos Testes Parametrizados

Testes parametrizados são uma técnica poderosa no PHPUnit que permite executar o mesmo método de teste com diferentes conjuntos de dados. Em vez de escrever múltiplos métodos de teste para cada cenário possível, você define um único método que recebe parâmetros e um "data provider" que fornece os valores.

O principal problema que resolvem é a redução de código repetitivo. Imagine testar uma função de validação de CPF com 10 entradas diferentes — sem data providers, você teria 10 métodos quase idênticos. Com eles, tudo se resume a um único método e um array de dados.

A sintaxe básica no PHPUnit envolve a anotação @dataProvider e um método público que retorna um array de arrays.

2. Criando seu Primeiro Data Provider

A estrutura fundamental de um data provider é um método público que retorna um array. Cada elemento do array é outro array contendo os argumentos que serão passados para o método de teste.

<?php
use PHPUnit\Framework\TestCase;

class CpfValidatorTest extends TestCase
{
    /**
     * @dataProvider cpfProvider
     */
    public function testValidarCpf(string $cpf, bool $esperado): void
    {
        $validador = new CpfValidator();
        $this->assertEquals($esperado, $validador->validar($cpf));
    }

    public function cpfProvider(): array
    {
        return [
            ['529.982.247-25', true],
            ['111.111.111-11', false],
            ['123.456.789-09', false],
            ['000.000.000-00', false],
        ];
    }
}

O data provider cpfProvider retorna quatro casos de teste. Cada caso contém um CPF e o resultado esperado. O método testValidarCpf recebe esses valores automaticamente.

3. Formatos de Retorno do Data Provider

O formato mais comum é o array indexado numericamente, como visto acima. No entanto, você pode usar arrays associativos para nomear cada caso:

public function cpfProvider(): array
{
    return [
        'cpf_valido' => ['529.982.247-25', true],
        'cpf_digitos_iguais' => ['111.111.111-11', false],
        'cpf_invalido_qualquer' => ['123.456.789-09', false],
    ];
}

Para conjuntos de dados muito grandes, iterators e geradores com yield são mais eficientes em termos de memória:

public function cpfProvider(): iterable
{
    yield 'cpf_valido' => ['529.982.247-25', true];
    yield 'cpf_digitos_iguais' => ['111.111.111-11', false];
    // ... mais casos
}

4. Trabalhando com Múltiplos Parâmetros

Data providers podem fornecer quantos parâmetros forem necessários. Veja um exemplo com uma calculadora:

<?php
use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    /**
     * @dataProvider operacoesProvider
     */
    public function testOperacoes(float $a, float $b, string $operacao, float $esperado): void
    {
        $calc = new Calculadora();
        $resultado = $calc->calcular($a, $b, $operacao);
        $this->assertEquals($esperado, $resultado);
    }

    public function operacoesProvider(): array
    {
        return [
            'soma_positivos' => [2, 3, 'soma', 5],
            'subtracao' => [10, 4, 'subtracao', 6],
            'multiplicacao' => [3, 4, 'multiplicacao', 12],
            'divisao' => [10, 2, 'divisao', 5],
            'soma_negativos' => [-5, -3, 'soma', -8],
        ];
    }
}

5. Nomeando e Organizando Conjuntos de Dados

Nomear cada caso de teste com chaves associativas traz benefícios significativos. Quando um teste falha, a mensagem de erro inclui o nome do conjunto de dados, facilitando a identificação do problema.

/**
 * @dataProvider valoresLimiteProvider
 */
public function testValidacaoIdade(int $idade, bool $esperado): void
{
    $validador = new ValidadorIdade();
    $this->assertEquals($esperado, $validador->validar($idade));
}

public function valoresLimiteProvider(): array
{
    return [
        'menor_de_idade' => [17, false],
        'exatamente_18' => [18, true],
        'adulto_jovem' => [25, true],
        'idoso' => [65, true],
        'acima_de_120' => [121, false],
    ];
}

Boas práticas incluem usar nomes descritivos que expliquem o cenário, manter um padrão consistente (snake_case ou camelCase) e evitar nomes genéricos como "caso1", "caso2".

6. Data Providers com Objetos e Dados Complexos

Data providers podem retornar objetos, arrays complexos e instâncias de classes. Isso é útil para testar cenários de criação de entidades:

<?php
use PHPUnit\Framework\TestCase;

class UsuarioFactoryTest extends TestCase
{
    /**
     * @dataProvider usuarioProvider
     */
    public function testCriarUsuario(string $nome, string $email, array $roles, bool $esperado): void
    {
        $factory = new UsuarioFactory();
        $usuario = $factory->criar($nome, $email, $roles);

        $this->assertInstanceOf(Usuario::class, $usuario);
        $this->assertEquals($esperado, $usuario->isAtivo());
    }

    public function usuarioProvider(): array
    {
        $adminRoles = ['ROLE_ADMIN', 'ROLE_USER'];
        $userRoles = ['ROLE_USER'];

        return [
            'usuario_admin' => ['João', 'joao@email.com', $adminRoles, true],
            'usuario_comum' => ['Maria', 'maria@email.com', $userRoles, true],
            'usuario_sem_roles' => ['Pedro', 'pedro@email.com', [], false],
        ];
    }
}

Para cenários Doctrine com múltiplos atributos:

public function entidadeProvider(): array
{
    $enderecoValido = new Endereco('Rua A', '123', 'São Paulo');
    $enderecoSemNumero = new Endereco('Rua B', '', 'Rio de Janeiro');

    return [
        'entidade_completa' => [
            new Produto('Notebook', 2500.00, $enderecoValido, true),
            true
        ],
        'entidade_sem_endereco' => [
            new Produto('Mouse', 50.00, null, false),
            false
        ],
    ];
}

7. Boas Práticas e Armadilhas Comuns

Isolamento e reutilização: Mantenha data providers em métodos separados ou traits para reutilização entre diferentes classes de teste.

trait UsuarioDataProviderTrait
{
    public function usuarioBasicoProvider(): array
    {
        return [
            'usuario_valido' => ['João', 'joao@email.com', '123456'],
            'usuario_email_invalido' => ['Maria', 'email-invalido', '123456'],
        ];
    }
}

Evite lógica complexa: Data providers devem ser simples e previsíveis. Lógica condicional ou loops complexos dentro deles dificultam a manutenção e a depuração.

Performance: Para grandes volumes de dados (centenas ou milhares de casos), prefira geradores com yield para evitar consumo excessivo de memória.

PHPUnit 10+: A partir do PHPUnit 10, você pode usar o atributo #[TestWith] para casos inline simples:

#[TestWith([2, 3, 5])]
#[TestWith([0, 0, 0])]
#[TestWith([-1, -1, -2])]
public function testSoma(int $a, int $b, int $esperado): void
{
    $this->assertEquals($esperado, $a + $b);
}

8. Integração com Outros Recursos de Teste

Data providers combinam perfeitamente com mocks e stubs. Você pode criar mocks dentro do data provider para cenários específicos:

public function servicoProvider(): array
{
    $mockRepositorio = $this->createMock(Repositorio::class);
    $mockRepositorio->method('buscar')->willReturn(['dados' => 'valor']);

    return [
        'com_repositorio_mockado' => [$mockRepositorio, 'entrada', 'esperado'],
    ];
}

Em testes de integração com banco de dados, data providers podem fornecer diferentes estados iniciais:

/**
 * @dataProvider usuarioBancoProvider
 */
public function testBuscarUsuarioPorEmail(string $email, ?array $esperado): void
{
    $this->inserirDadosNoBanco($email);
    $repositorio = new UsuarioRepository($this->entityManager);
    $resultado = $repositorio->buscarPorEmail($email);

    if ($esperado === null) {
        $this->assertNull($resultado);
    } else {
        $this->assertNotNull($resultado);
    }
}

A combinação com @depends permite criar pipelines de teste onde a saída de um teste alimenta o próximo, enquanto data providers garantem múltiplas entradas:

/**
 * @dataProvider entradaProvider
 */
public function testProcessarEntrada(array $dados): array
{
    $processador = new Processador();
    return $processador->processar($dados);
}

/**
 * @depends testProcessarEntrada
 */
public function testValidarSaida(array $resultado): void
{
    $this->assertArrayHasKey('status', $resultado);
    $this->assertEquals('sucesso', $resultado['status']);
}

Referências