Help us improve
Share bugs, ideas, or general feedback.
From perl-development
Guides Perl testing with Test::More assertions, prove execution, test suites, TAP output, Test::Class, and deep structure comparisons.
npx claudepluginhub jamie-bitflight/claude_skills --plugin perl-developmentHow this skill is triggered — by the user, by Claude, or both
Slash command
/perl-development:perl-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive guide for testing Perl code using Test::More, Test::Class, and related modules.
Provides Perl testing patterns with Test2::V0, Test::More, prove runner, Devel::Cover coverage, mocking, and TDD for new code, suites, reviews, and debugging.
Perl testing patterns using Test2::V0, Test::More, prove runner, mocking, coverage with Devel::Cover, and TDD methodology.
Guides modern Perl 5.30+ script development with pragmas (strict, warnings, autodie), error handling (Try::Tiny), signatures, and standard modules.
Share bugs, ideas, or general feedback.
Comprehensive guide for testing Perl code using Test::More, Test::Class, and related modules.
The foundation of Perl testing.
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
# Basic assertions
ok(1, 'truth is true');
ok(!0, 'false is not true');
# Equality
is($got, $expected, 'values are equal');
isnt($got, $unexpected, 'values differ');
# String comparison
is($string, 'expected', 'string matches');
like($string, qr/pattern/, 'matches regex');
unlike($string, qr/bad/, 'does not match regex');
# Numeric comparison
cmp_ok($num, '>', 10, 'greater than 10');
cmp_ok($num, '==', 42, 'equals 42');
# Data structures
is_deeply(\@got, \@expected, 'arrays match');
is_deeply(\%got, \%expected, 'hashes match');
done_testing();
# Declare expected test count
use Test::More tests => 5;
# Or count at end
use Test::More;
# ... tests ...
done_testing();
# Skip remaining tests
use Test::More;
# ... tests ...
done_testing(10); # Explicit count
# Run all tests in t/
prove
# Verbose output
prove -v
# Run specific test
prove t/basic.t
# Recursive with color
prove -r --color
# With library path
prove -l t/ # Adds lib/ to @INC
# Parallel execution
prove -j4
# Shuffle order
prove --shuffle
| Flag | Purpose |
|---|---|
-v | Verbose TAP output |
-l | Add lib/ to @INC |
-b | Add blib/ to @INC |
-r | Recursive directory search |
-j N | Run N tests in parallel |
--color | Colored output |
--shuffle | Randomize test order |
--state=save | Save test state |
project/
├── lib/
│ └── MyApp/
│ ├── Module.pm
│ └── Utils.pm
├── t/
│ ├── 00-compile.t
│ ├── 01-basic.t
│ ├── 02-module.t
│ └── lib/
│ └── Test/
│ └── MyApp.pm
└── xt/
├── author/
│ └── pod.t
└── release/
└── manifest.t
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use_ok('MyApp::Module');
use_ok('MyApp::Utils');
done_testing();
# Boolean
ok($condition, $description);
# Equality
is($got, $expected, $desc); # String comparison
isnt($got, $expected, $desc);
cmp_ok($got, $op, $expected, $desc); # Any operator
# Pattern matching
like($got, qr/pattern/, $desc);
unlike($got, qr/pattern/, $desc);
# Data structures
is_deeply($got, $expected, $desc);
# Reference type
isa_ok($obj, 'ClassName');
can_ok($obj, 'method1', 'method2');
# Pass/fail
pass($desc);
fail($desc);
# Additional output on failure
is($got, $expected, 'test') or diag("Got: $got");
# Always print
note("Debug info: $value");
# Dump structure
use Data::Dumper;
diag(Dumper($complex_structure));
# Skip tests conditionally
SKIP: {
skip "No database connection", 3 unless $db;
ok($db->ping, 'database responds');
is($db->version, '5.7', 'correct version');
ok($db->tables > 0, 'has tables');
}
# Mark tests as TODO
TODO: {
local $TODO = "Feature not implemented";
is(new_feature(), 'expected', 'new feature works');
}
# Skip all tests in file
plan skip_all => 'Module not installed' unless eval { require Optional::Module };
Test that code dies or lives correctly.
use Test::More;
use Test::Exception;
# Test that code dies
dies_ok { divide(1, 0) } 'division by zero dies';
# Test that code lives
lives_ok { safe_operation() } 'safe operation lives';
# Test specific exception
throws_ok { bad_call() } qr/invalid argument/i, 'throws expected error';
# Test exception type
throws_ok { bad_call() } 'MyApp::Exception', 'throws correct class';
# Combine with return value
lives_and { is(calc(2, 2), 4) } 'calc lives and returns correct value';
done_testing();
Deep structure comparison with flexibility.
use Test::More;
use Test::Deep;
# Ignore certain values
cmp_deeply(
$got,
{
id => ignore(), # Any value
name => 'test',
created => re(qr/^\d{4}-\d{2}-\d{2}$/), # Pattern match
},
'structure matches'
);
# Bag comparison (order doesn't matter)
cmp_deeply(
\@got,
bag(1, 2, 3), # Same elements, any order
'contains all elements'
);
# Subset matching
cmp_deeply(
$got,
superhashof({ required => 'value' }),
'contains required keys'
);
# Type checking
cmp_deeply(
$data,
{
count => code(sub { $_[0] > 0 }),
items => array_each(isa('MyApp::Item')),
},
'types correct'
);
done_testing();
Mock module behavior for isolation.
use Test::More;
use Test::MockModule;
# Mock a module
my $mock = Test::MockModule->new('MyApp::Database');
# Replace a method
$mock->mock('connect', sub { return 'fake_handle' });
# Mock with return value
$mock->mock('fetch', sub { return { id => 1, name => 'test' } });
# Verify mock was called
my $called = 0;
$mock->mock('save', sub { $called++; return 1 });
# Run code under test
my $result = MyApp::Service->new->process();
is($called, 1, 'save was called');
# Restore original
$mock->unmock('connect');
done_testing();
Object-oriented testing with setup/teardown.
package Test::MyApp::User;
use parent 'Test::Class';
use Test::More;
use MyApp::User;
# Run before each test method
sub setup : Test(setup) {
my $self = shift;
$self->{user} = MyApp::User->new(name => 'Test');
}
# Run after each test method
sub teardown : Test(teardown) {
my $self = shift;
$self->{user} = undef;
}
# Test methods
sub test_creation : Test(2) {
my $self = shift;
isa_ok($self->{user}, 'MyApp::User');
is($self->{user}->name, 'Test', 'name is set');
}
sub test_validation : Test(1) {
my $self = shift;
ok($self->{user}->is_valid, 'user is valid');
}
# Run all Test::Class tests
Test::Class->runtests;
#!/usr/bin/env perl
# t/run_all.t
use strict;
use warnings;
use lib 't/lib';
use Test::MyApp::User;
use Test::MyApp::Order;
Test::Class->runtests;
use Path::Tiny;
use JSON::PP;
sub load_fixture {
my ($name) = @_;
my $file = path("t/fixtures/$name.json");
return decode_json($file->slurp_utf8);
}
# In test
my $data = load_fixture('users');
use Test::More;
use DBI;
my $dbh;
sub setup_test_db {
$dbh = DBI->connect('dbi:SQLite::memory:', '', '');
$dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
$dbh->do("INSERT INTO users VALUES (1, 'test')");
}
sub teardown_test_db {
$dbh->disconnect if $dbh;
}
# Use in tests
setup_test_db();
# ... run tests ...
teardown_test_db();
# Install Devel::Cover
cpanm Devel::Cover
# Run tests with coverage
cover -test
# Generate HTML report
cover -report html
# View report
open cover_db/coverage.html
For modern Perl coding patterns, see the perl-development skill.