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
- Ao escrever novo código Perl (siga TDD: red, green, refactor)
- Ao projetar suites de teste para módulos ou aplicações Perl
- Ao revisar a cobertura de testes Perl
- Ao configurar infraestrutura de teste Perl
- Ao migrar testes de Test::More para Test2::V0
- Ao depurar testes Perl com falha
Fluxo de Trabalho TDD
Sempre siga o ciclo RED-GREEN-REFACTOR.
# Passo 1: RED — Escreva um teste com falha
# t/unit/calculator.t
use v5.36;
use Test2::V0;
use lib 'lib';
use Calculator;
subtest 'soma' => sub {
my $calc = Calculator->new;
is($calc->add(2, 3), 5, 'soma dois números');
is($calc->add(-1, 1), 0, 'trata negativos');
};
done_testing;
# Passo 2: GREEN — Escreva a 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: REFACTOR — Melhore enquanto os testes permanecem verdes
# Execute: prove -lv t/unit/calculator.t
Fundamentos do Test::More
Módulo de teste Perl padrão — amplamente utilizado, incluído no core.
Assertions Básicas
use v5.36;
use Test::More;
# Planeje antecipadamente ou use done_testing
# plan tests => 5; # Plano fixo (opcional)
# Igualdade
is($resultado, 42, 'retorna o 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 a 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 a senha');
# Verificação de tipo
isa_ok($obj, 'MinhaApp::Usuario');
can_ok($obj, 'salvar', 'excluir');
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;
Test2::V0 Framework Moderno
Test2::V0 é o substituto moderno para Test::More — assertions mais ricas, melhores diagnósticos e extensível.
Por que Test2?
- Comparação profunda superior com construtores de 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 de hash — verifica estrutura parcial
is(
$usuario->to_hash,
hash {
field nome => 'Alice';
field email => match(qr/\(a)example\.com$/);
field idade => validator(sub { $_ >= 18 });
# Ignorar outros campos
etc();
},
'usuário tem campos esperados'
);
# Construtor de array
is(
$resultado,
array {
item 'primeiro';
item match(qr/^segundo/);
item DNE(); # Não Existe — verifica 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 = Usuario->new(nome => 'Alice', email => '[email protected]');
ok($usuario, 'objeto 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 {
Usuario->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;
# Testa se o código morre
like(
dies { dividir(10, 0) },
qr/Divisão por zero/,
'morre na divisão por zero'
);
# Testa se o código vive
ok(lives { dividir(10, 2) }, 'divisão bem-sucedida') or note($@);
# Padrão combinado
subtest 'tratamento de erros' => sub {
ok(lives { parse_config('valido.json') }, 'config válida parseia');
like(
dies { parse_config('ausente.json') },
qr/Não é possível abrir/,
'arquivo ausente morre com mensagem'
);
};
done_testing;
Organização de Testes e prove
Estrutura de Diretórios
t/
├── 00-carregar.t # Verifica se os módulos compilam
├── 01-basico.t # Funcionalidade principal
├── unit/
│ ├── config.t # Testes unitários por módulo
│ ├── usuario.t
│ └── util.t
├── integration/
│ ├── database.t
│ └── api.t
├── lib/
│ └── TestHelper.pm # Utilitários de teste compartilhados
└── fixtures/
├── config.json # Arquivos de dados de teste
└── usuarios.csv
Comandos prove
# Executar todos os testes
prove -l t/
# Saída verbosa
prove -lv t/
# Executar teste específico
prove -lv t/unit/usuario.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 temporizador
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_arquivo("$arquivo");
is($resultado->{conta_linhas}, 3, 'conta linhas');
# Teardown acontece 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.
Mocks (Simulação)
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'mock de API externa' => sub {
my $mock = Test::MockModule->new('MinhaApp::API');
# Bom: Mock retorna dados controlados
$mock->mock(fetch_usuario => sub ($self, $id) {
return { id => $id, nome => 'Usuário Mock', email => '[email protected]' };
});
my $api = MinhaApp::API->new;
my $usuario = $api->fetch_usuario(42);
is($usuario->{nome}, 'Usuário Mock', 'retorna usuário mockado');
# Verificar contagem de chamadas
my $contagem_chamadas = 0;
$mock->mock(fetch_usuario => sub { $contagem_chamadas++; return {} });
$api->fetch_usuario(1);
$api->fetch_usuario(2);
is($contagem_chamadas, 2, 'fetch_usuario chamado duas vezes');
# Mock é automaticamente restaurado quando $mock sai de escopo
};
# Ruim: Monkey-patching sem restauração
# *MinhaApp::API::fetch_usuario = 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/usuario.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: falha abaixo do limiar
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Teste 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 (red-green-refactor)
- Use Test2::V0: Assertions modernas, melhores diagnósticos
- Use subtests: Agrupe assertions 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 de usuário com senha inválida falha' - Teste casos de borda: Strings vazias, undef, zero, valores limite
- Busque 80%+ de cobertura: Foco em caminhos de lógica de negócio
- Mantenha os testes rápidos: Mock I/O, use bancos em memória
NÃO FAÇA
- Não teste implementação: Teste comportamento e saída, não detalhes 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 use mock em excesso: Simule apenas fronteiras, não o código sob teste
- Não use
Test::Moreem projetos novos: 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 verboso | prove -lv t/unit/usuario.t |
| Execução em paralelo | 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') |
| Simular um método | Test::MockModule->new('Pkg')->mock(m => sub { ... }) |
| Pular testes | SKIP: { skip 'motivo', $contagem a menos que $cond; ... } |
| Testes TODO | TODO: { local $TODO = 'motivo'; ... } |
Armadilhas Comuns
Esquecer done_testing
# Ruim: O arquivo de teste executa 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 Ausente
# Ruim: Módulos em lib/ não encontrados
prove t/unit/usuario.t
# Não é possível localizar MinhaApp/Usuario.pm em @INC
# Bom: Inclua lib/ em @INC
prove -l t/unit/usuario.t
Excesso de Mock
Simule a dependência, não o código sob teste. Se o seu teste apenas verifica que um mock retorna o que você disse a ele, ele não testa nada.
Poluição de Testes
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 executar e Devel::Cover para responsabilidade.