Padrões de Teste em Perl
Estratégias abrangentes de teste para aplicações Perl usando Test2::V0, Test::More, prove e metodologia TDD.
Quando Ativar
- Escrevendo novo código Perl (siga TDD: vermelho, verde, refatorar)
- Projetando suítes de teste para módulos ou aplicações Perl
- Revisando cobertura de teste Perl
- Configurando infraestrutura de teste Perl
- Migrando testes de Test::More para Test2::V0
- Depurando testes Perl com falha
Fluxo de Trabalho TDD
Sempre siga o ciclo VERMELHO-VERDE-REFATORAR.
# Passo 1: VERMELHO — Escreva um teste com falha
# t/unit/calculator.t
use v5.36;
use Test2::V0;
use lib 'lib';
use Calculator;
subtest 'adição' => sub {
my $calc = Calculator->new;
is($calc->add(2, 3), 5, 'soma dois números');
is($calc->add(-1, 1), 0, 'lida com negativos');
};
done_testing;
# Passo 2: VERDE — Escreva implementação mínima
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# Passo 3: REFATORAR — Melhore enquanto os testes permanecem verdes
# Execute: prove -lv t/unit/calculator.t
Fundamentos do Test::More
O módulo de teste padrão do Perl — amplamente utilizado, acompanha o core.
Asserções Básicas
use v5.36;
use Test::More;
# Planeje com antecedência ou use done_testing
# plan tests => 5; # Plano fixo (opcional)
# Igualdade
is($resultado, 42, 'retorna valor correto');
isnt($resultado, 0, 'não é zero');
# Booleano
ok($usuario->is_active, 'usuário está ativo');
ok(!$usuario->is_banned, 'usuário não está banido');
# Comparação profunda
is_deeply(
$obtido,
{ nome => 'Alice', papeis => ['admin'] },
'retorna estrutura esperada'
);
# Correspondência de padrão
like($erro, qr/não encontrado/i, 'erro menciona não encontrado');
unlike($saida, qr/senha/, 'saída oculta senha');
# Verificação de tipo
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'salvar', 'deletar');
done_testing;
SKIP e TODO
use v5.36;
use Test::More;
# Pular testes condicionalmente
SKIP: {
skip 'Nenhum banco de dados configurado', 2 a menos que $ENV{TEST_DB};
my $db = connect_db();
ok($db->ping, 'banco de dados está acessível');
is($db->version, '15', 'versão correta do PostgreSQL');
}
# Marcar falhas esperadas
TODO: {
local $TODO = 'Cache ainda não implementado';
is($cache->get('chave'), 'valor', 'cache retorna valor');
}
done_testing;
Framework Moderno Test2::V0
Test2::V0 é o substituto moderno para Test::More — asserções mais ricas, melhores diagnósticos e extensível.
Por que Test2?
- Comparação profunda superior com construtores hash/array
- Melhor saída de diagnóstico em falhas
- Subtests com escopo mais limpo
- Extensível via plugins Test2::Tools::*
- Compatível com versões anteriores de testes Test::More
Comparação Profunda com Construtores
use v5.36;
use Test2::V0;
# Construtor hash — verificar estrutura parcial
is(
$usuario->to_hash,
hash {
field nome => 'Alice';
field email => match(qr/\@example\.com$/);
field idade => validator(sub { $_ >= 18 });
# Ignorar outros campos
etc();
},
'usuário tem campos esperados'
);
# Construtor array
is(
$resultado,
array {
item 'primeiro';
item match(qr/^segundo/);
item DNE(); # Does Not Exist — verificar se não há itens extras
},
'resultado corresponde à lista esperada'
);
# Bag — comparação independente de ordem
is(
$tags,
bag {
item 'perl';
item 'teste';
item 'tdd';
},
'tem todas as tags necessárias independente da ordem'
);
Subtests
use v5.36;
use Test2::V0;
subtest 'Criação de Usuário' => sub {
my $usuario = User->new(nome => 'Alice', email => '[email protected]');
ok($usuario, 'objeto de usuário criado');
is($usuario->nome, 'Alice', 'nome está definido');
is($usuario->email, '[email protected]', 'email está definido');
};
subtest 'Validação de Usuário' => sub {
my $avisos = warns {
User->new(nome => '', email => 'ruim');
};
ok($avisos, 'avisa sobre dados inválidos');
};
done_testing;
Teste de Exceções com Test2
use v5.36;
use Test2::V0;
# Testar se o código morre
like(
dies { divide(10, 0) },
qr/Divisão por zero/,
'morre na divisão por zero'
);
# Testar se o código vive
ok(lives { divide(10, 2) }, 'divisão bem-sucedida') or note($@);
# Padrão combinado
subtest 'tratamento de erros' => sub {
ok(lives { parse_config('válido.json') }, 'configuração válida é analisada');
like(
dies { parse_config('faltando.json') },
qr/Não é possível abrir/,
'arquivo faltando morre com mensagem'
);
};
done_testing;
Organização de Testes e prove
Estrutura de Diretórios
t/
├── 00-load.t # Verificar compilação dos módulos
├── 01-basic.t # Funcionalidade principal
├── unit/
│ ├── config.t # Testes unitários por módulo
│ ├── user.t
│ └── util.t
├── integration/
│ ├── database.t
│ └── api.t
├── lib/
│ └── TestHelper.pm # Utilitários de teste compartilhados
└── fixtures/
├── config.json # Arquivos de dados de teste
└── users.csv
Comandos prove
# Executar todos os testes
prove -l t/
# Saída detalhada
prove -lv t/
# Executar teste específico
prove -lv t/unit/user.t
# Busca recursiva
prove -lr t/
# Execução paralela (8 jobs)
prove -lr -j8 t/
# Executar apenas testes com falha da última execução
prove -l --state=failed t/
# Saída colorida com timer
prove -l --color --timer t/
# Saída TAP para CI
prove -l --formatter TAP::Formatter::JUnit t/ > resultados.xml
Configuração .proverc
-l
--color
--timer
-r
-j4
--state=save
Fixtures e Setup/Teardown
Isolamento de Subtest
use v5.36;
use Test2::V0;
use File::Temp qw(tempdir);
use Path::Tiny;
subtest 'processamento de arquivo' => sub {
# Setup
my $dir = tempdir(CLEANUP => 1);
my $arquivo = path($dir, 'entrada.txt');
$arquivo->spew_utf8("linha1\nlinha2\nlinha3\n");
# Teste
my $resultado = process_file("$arquivo");
is($resultado->{contagem_linhas}, 3, 'conta linhas');
# Teardown ocorre automaticamente (CLEANUP => 1)
};
Helpers de Teste Compartilhados
Coloque helpers reutilizáveis em t/lib/TestHelper.pm e carregue com use lib 't/lib'. Exporte funções de fábrica como create_test_db(), create_temp_dir() e fixture_path() via Exporter.
Mocking
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'mock API externa' => sub {
my $mock = Test::MockModule->new('MyApp::API');
# Bom: Mock retorna dados controlados
$mock->mock(fetch_user => sub ($self, $id) {
return { id => $id, nome => 'Usuário Mock', email => '[email protected]' };
});
my $api = MyApp::API->new;
my $usuario = $api->fetch_user(42);
is($usuario->{nome}, 'Usuário Mock', 'retorna usuário mockado');
# Verificar contagem de chamadas
my $contagem_chamadas = 0;
$mock->mock(fetch_user => sub { $contagem_chamadas++; return {} });
$api->fetch_user(1);
$api->fetch_user(2);
is($contagem_chamadas, 2, 'fetch_user chamado duas vezes');
# Mock é automaticamente restaurado quando $mock sai de escopo
};
# Ruim: Monkey-patching sem restauração
# *MyApp::API::fetch_user = sub { ... }; # NUNCA — vaza entre testes
Para objetos mock leves, use Test::MockObject para criar dublês de teste injetáveis com ->mock() e verifique chamadas com ->called_ok().
Cobertura com Devel::Cover
Executando Cobertura
# Relatório básico de cobertura
cover -test
# Ou passo a passo
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# Relatório HTML
cover -report html
open cover_db/coverage.html
# Limiares específicos
cover -test -report text | grep 'Total'
# Amigável para CI: falhar abaixo do limiar
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Testes de Integração
Use SQLite em memória para testes de banco de dados, mock HTTP::Tiny para testes de API.
use v5.36;
use Test2::V0;
use DBI;
subtest 'integração com banco de dados' => sub {
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
RaiseError => 1,
});
$dbh->do('CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT)');
$dbh->prepare('INSERT INTO usuarios (nome) VALUES (?)')->execute('Alice');
my $linha = $dbh->selectrow_hashref('SELECT * FROM usuarios WHERE nome = ?', undef, 'Alice');
is($linha->{nome}, 'Alice', 'usuário inserido e recuperado');
};
done_testing;
Melhores Práticas
FAÇA
- Siga TDD: Escreva testes antes da implementação (vermelho-verde-refatorar)
- Use Test2::V0: Asserções modernas, melhores diagnósticos
- Use subtests: Agrupe asserções relacionadas, isole estado
- Mock dependências externas: Rede, banco de dados, sistema de arquivos
- Use
prove -l: Sempre inclua lib/ em@INC - Nomeie testes claramente:
'login do usuário com senha inválida falha' - Teste casos de borda: Strings vazias, undef, zero, valores limite
- Mire em cobertura de 80%+: Foque em caminhos de lógica de negócio
- Mantenha testes rápidos: Mock I/O, use bancos de dados em memória
NÃO FAÇA
- Não teste implementação: Teste comportamento e saída, não internos
- Não compartilhe estado entre subtests: Cada subtest deve ser independente
- Não pule
done_testing: Garante que todos os testes planejados foram executados - Não exagere no mock: Mock apenas fronteiras, não o código sob teste
- Não use
Test::Morepara novos projetos: Prefira Test2::V0 - Não ignore falhas de teste: Todos os testes devem passar antes do merge
- Não teste módulos CPAN: Confie que as bibliotecas funcionam corretamente
- Não escreva testes frágeis: Evite correspondência de string excessivamente específica
Referência Rápida
| Tarefa | Comando / Padrão |
|---|---|
| Executar todos os testes | prove -lr t/ |
| Executar um teste detalhado | prove -lv t/unit/user.t |
| Execução paralela de testes | prove -lr -j8 t/ |
| Relatório de cobertura | cover -test && cover -report html |
| Testar igualdade | is($obtido, $esperado, 'rótulo') |
| Comparação profunda | is($obtido, hash { field k => 'v'; etc() }, 'rótulo') |
| Testar exceção | like(dies { ... }, qr/msg/, 'rótulo') |
| Testar sem exceção | ok(lives { ... }, 'rótulo') |
| Mock de método | Test::MockModule->new('Pkg')->mock(m => sub { ... }) |
| Pular testes | SKIP: { skip 'motivo', $count a menos que $cond; ... } |
| Testes TODO | TODO: { local $TODO = 'motivo'; ... } |
Armadilhas Comuns
Esquecer done_testing
# Ruim: O arquivo de teste é executado mas não verifica se todos os testes foram executados
use Test2::V0;
is(1, 1, 'funciona');
# Faltando done_testing — bugs silenciosos se o código de teste for pulado
# Bom: Sempre termine com done_testing
use Test2::V0;
is(1, 1, 'funciona');
done_testing;
Flag -l Faltando
# Ruim: Módulos em lib/ não encontrados
prove t/unit/user.t
# Can't locate MyApp/User.pm in @INC
# Bom: Incluir lib/ em @INC
prove -l t/unit/user.t
Excesso de Mock
Mock a dependência, não o código sob teste. Se seu teste apenas verifica que um mock retorna o que você disse a ele, ele não testa nada.
Poluição de Teste
Use variáveis my dentro de subtests — nunca our — para evitar vazamento de estado entre testes.
Lembre-se: Testes são sua rede de segurança. Mantenha-os rápidos, focados e independentes. Use Test2::V0 para novos projetos, prove para execução e Devel::Cover para responsabilidade.