Claude Agent Skill · by Affaan M

Perl Testing

Install Perl Testing skill for Claude Code from affaan-m/everything-claude-code.

Install
Terminal · npx
$npx skills add https://github.com/obra/superpowers --skill test-driven-development
Works with Paperclip

How Perl Testing fits into a Paperclip company.

Perl Testing drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md475 lines
Expand
---name: perl-testingdescription: Perl testing patterns using Test2::V0, Test::More, prove runner, mocking, coverage with Devel::Cover, and TDD methodology.origin: ECC--- # Perl Testing Patterns Comprehensive testing strategies for Perl applications using Test2::V0, Test::More, prove, and TDD methodology. ## When to Activate - Writing new Perl code (follow TDD: red, green, refactor)- Designing test suites for Perl modules or applications- Reviewing Perl test coverage- Setting up Perl testing infrastructure- Migrating tests from Test::More to Test2::V0- Debugging failing Perl tests ## TDD Workflow Always follow the RED-GREEN-REFACTOR cycle. ```perl# Step 1: RED — Write a failing test# t/unit/calculator.tuse v5.36;use Test2::V0; use lib 'lib';use Calculator; subtest 'addition' => sub {    my $calc = Calculator->new;    is($calc->add(2, 3), 5, 'adds two numbers');    is($calc->add(-1, 1), 0, 'handles negatives');}; done_testing; # Step 2: GREEN — Write minimal implementation# lib/Calculator.pmpackage Calculator;use v5.36;use Moo; sub add($self, $a, $b) {    return $a + $b;} 1; # Step 3: REFACTOR — Improve while tests stay green# Run: prove -lv t/unit/calculator.t``` ## Test::More Fundamentals The standard Perl testing module — widely used, ships with core. ### Basic Assertions ```perluse v5.36;use Test::More; # Plan upfront or use done_testing# plan tests => 5;  # Fixed plan (optional) # Equalityis($result, 42, 'returns correct value');isnt($result, 0, 'not zero'); # Booleanok($user->is_active, 'user is active');ok(!$user->is_banned, 'user is not banned'); # Deep comparisonis_deeply(    $got,    { name => 'Alice', roles => ['admin'] },    'returns expected structure'); # Pattern matchinglike($error, qr/not found/i, 'error mentions not found');unlike($output, qr/password/, 'output hides password'); # Type checkisa_ok($obj, 'MyApp::User');can_ok($obj, 'save', 'delete'); done_testing;``` ### SKIP and TODO ```perluse v5.36;use Test::More; # Skip tests conditionallySKIP: {    skip 'No database configured', 2 unless $ENV{TEST_DB};     my $db = connect_db();    ok($db->ping, 'database is reachable');    is($db->version, '15', 'correct PostgreSQL version');} # Mark expected failuresTODO: {    local $TODO = 'Caching not yet implemented';    is($cache->get('key'), 'value', 'cache returns value');} done_testing;``` ## Test2::V0 Modern Framework Test2::V0 is the modern replacement for Test::More — richer assertions, better diagnostics, and extensible. ### Why Test2? - Superior deep comparison with hash/array builders- Better diagnostic output on failures- Subtests with cleaner scoping- Extensible via Test2::Tools::* plugins- Backward-compatible with Test::More tests ### Deep Comparison with Builders ```perluse v5.36;use Test2::V0; # Hash builder — check partial structureis(    $user->to_hash,    hash {        field name  => 'Alice';        field email => match(qr/\@example\.com$/);        field age   => validator(sub { $_ >= 18 });        # Ignore other fields        etc();    },    'user has expected fields'); # Array builderis(    $result,    array {        item 'first';        item match(qr/^second/);        item DNE();  # Does Not Exist — verify no extra items    },    'result matches expected list'); # Bag — order-independent comparisonis(    $tags,    bag {        item 'perl';        item 'testing';        item 'tdd';    },    'has all required tags regardless of order');``` ### Subtests ```perluse v5.36;use Test2::V0; subtest 'User creation' => sub {    my $user = User->new(name => 'Alice', email => 'alice@example.com');    ok($user, 'user object created');    is($user->name, 'Alice', 'name is set');    is($user->email, 'alice@example.com', 'email is set');}; subtest 'User validation' => sub {    my $warnings = warns {        User->new(name => '', email => 'bad');    };    ok($warnings, 'warns on invalid data');}; done_testing;``` ### Exception Testing with Test2 ```perluse v5.36;use Test2::V0; # Test that code dieslike(    dies { divide(10, 0) },    qr/Division by zero/,    'dies on division by zero'); # Test that code livesok(lives { divide(10, 2) }, 'division succeeds') or note($@); # Combined patternsubtest 'error handling' => sub {    ok(lives { parse_config('valid.json') }, 'valid config parses');    like(        dies { parse_config('missing.json') },        qr/Cannot open/,        'missing file dies with message'    );}; done_testing;``` ## Test Organization and prove ### Directory Structure ```textt/├── 00-load.t              # Verify modules compile├── 01-basic.t             # Core functionality├── unit/│   ├── config.t           # Unit tests by module│   ├── user.t│   └── util.t├── integration/│   ├── database.t│   └── api.t├── lib/│   └── TestHelper.pm      # Shared test utilities└── fixtures/    ├── config.json        # Test data files    └── users.csv``` ### prove Commands ```bash# Run all testsprove -l t/ # Verbose outputprove -lv t/ # Run specific testprove -lv t/unit/user.t # Recursive searchprove -lr t/ # Parallel execution (8 jobs)prove -lr -j8 t/ # Run only failing tests from last runprove -l --state=failed t/ # Colored output with timerprove -l --color --timer t/ # TAP output for CIprove -l --formatter TAP::Formatter::JUnit t/ > results.xml``` ### .proverc Configuration ```text-l--color--timer-r-j4--state=save``` ## Fixtures and Setup/Teardown ### Subtest Isolation ```perluse v5.36;use Test2::V0;use File::Temp qw(tempdir);use Path::Tiny; subtest 'file processing' => sub {    # Setup    my $dir = tempdir(CLEANUP => 1);    my $file = path($dir, 'input.txt');    $file->spew_utf8("line1\nline2\nline3\n");     # Test    my $result = process_file("$file");    is($result->{line_count}, 3, 'counts lines');     # Teardown happens automatically (CLEANUP => 1)};``` ### Shared Test Helpers Place reusable helpers in `t/lib/TestHelper.pm` and load with `use lib 't/lib'`. Export factory functions like `create_test_db()`, `create_temp_dir()`, and `fixture_path()` via `Exporter`. ## Mocking ### Test::MockModule ```perluse v5.36;use Test2::V0;use Test::MockModule; subtest 'mock external API' => sub {    my $mock = Test::MockModule->new('MyApp::API');     # Good: Mock returns controlled data    $mock->mock(fetch_user => sub ($self, $id) {        return { id => $id, name => 'Mock User', email => 'mock@test.com' };    });     my $api = MyApp::API->new;    my $user = $api->fetch_user(42);    is($user->{name}, 'Mock User', 'returns mocked user');     # Verify call count    my $call_count = 0;    $mock->mock(fetch_user => sub { $call_count++; return {} });    $api->fetch_user(1);    $api->fetch_user(2);    is($call_count, 2, 'fetch_user called twice');     # Mock is automatically restored when $mock goes out of scope}; # Bad: Monkey-patching without restoration# *MyApp::API::fetch_user = sub { ... };  # NEVER — leaks across tests``` For lightweight mock objects, use `Test::MockObject` to create injectable test doubles with `->mock()` and verify calls with `->called_ok()`. ## Coverage with Devel::Cover ### Running Coverage ```bash# Basic coverage reportcover -test # Or step by stepperl -MDevel::Cover -Ilib t/unit/user.tcover # HTML reportcover -report htmlopen cover_db/coverage.html # Specific thresholdscover -test -report text | grep 'Total' # CI-friendly: fail under thresholdcover -test && cover -report text -select '^lib/' \  | perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'``` ### Integration Testing Use in-memory SQLite for database tests, mock HTTP::Tiny for API tests. ```perluse v5.36;use Test2::V0;use DBI; subtest 'database integration' => sub {    my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {        RaiseError => 1,    });    $dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');     $dbh->prepare('INSERT INTO users (name) VALUES (?)')->execute('Alice');    my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE name = ?', undef, 'Alice');    is($row->{name}, 'Alice', 'inserted and retrieved user');}; done_testing;``` ## Best Practices ### DO - **Follow TDD**: Write tests before implementation (red-green-refactor)- **Use Test2::V0**: Modern assertions, better diagnostics- **Use subtests**: Group related assertions, isolate state- **Mock external dependencies**: Network, database, file system- **Use `prove -l`**: Always include lib/ in `@INC`- **Name tests clearly**: `'user login with invalid password fails'`- **Test edge cases**: Empty strings, undef, zero, boundary values- **Aim for 80%+ coverage**: Focus on business logic paths- **Keep tests fast**: Mock I/O, use in-memory databases ### DON'T - **Don't test implementation**: Test behavior and output, not internals- **Don't share state between subtests**: Each subtest should be independent- **Don't skip `done_testing`**: Ensures all planned tests ran- **Don't over-mock**: Mock boundaries only, not the code under test- **Don't use `Test::More` for new projects**: Prefer Test2::V0- **Don't ignore test failures**: All tests must pass before merge- **Don't test CPAN modules**: Trust libraries to work correctly- **Don't write brittle tests**: Avoid over-specific string matching ## Quick Reference | Task | Command / Pattern ||---|---|| Run all tests | `prove -lr t/` || Run one test verbose | `prove -lv t/unit/user.t` || Parallel test run | `prove -lr -j8 t/` || Coverage report | `cover -test && cover -report html` || Test equality | `is($got, $expected, 'label')` || Deep comparison | `is($got, hash { field k => 'v'; etc() }, 'label')` || Test exception | `like(dies { ... }, qr/msg/, 'label')` || Test no exception | `ok(lives { ... }, 'label')` || Mock a method | `Test::MockModule->new('Pkg')->mock(m => sub { ... })` || Skip tests | `SKIP: { skip 'reason', $count unless $cond; ... }` || TODO tests | `TODO: { local $TODO = 'reason'; ... }` | ## Common Pitfalls ### Forgetting `done_testing` ```perl# Bad: Test file runs but doesn't verify all tests executeduse Test2::V0;is(1, 1, 'works');# Missing done_testing — silent bugs if test code is skipped # Good: Always end with done_testinguse Test2::V0;is(1, 1, 'works');done_testing;``` ### Missing `-l` Flag ```bash# Bad: Modules in lib/ not foundprove t/unit/user.t# Can't locate MyApp/User.pm in @INC # Good: Include lib/ in @INCprove -l t/unit/user.t``` ### Over-Mocking Mock the *dependency*, not the code under test. If your test only verifies that a mock returns what you told it to, it tests nothing. ### Test Pollution Use `my` variables inside subtests — never `our` — to prevent state leaking between tests. **Remember**: Tests are your safety net. Keep them fast, focused, and independent. Use Test2::V0 for new projects, prove for running, and Devel::Cover for accountability.