Source profileQuality 91/100

affaan-m/ECC/skills/perl-testing/SKILL.md

perl-testing

Perl testing patterns using Test2::V0, Test::More, prove runner, mocking, coverage with Devel::Cover, and TDD methodology.

Source repository stars
234,327
Declared platforms
0
Static risk flags
0
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

Comprehensive testing strategies for Perl applications using Test2::V0, Test::More, prove, and TDD methodology.

Best for

    Not for

    • Forgetting donetesting
    • Missing -l Flag

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "skills/perl-testing"
    Safe inspection promptEditorial

    Inspect the Agent Skill "perl-testing" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/skills/perl-testing/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      TDD Workflow

      Always follow the RED-GREEN-REFACTOR cycle.

      Always follow the RED-GREEN-REFACTOR cycle.
    2. 02

      Step 1: RED — Write a failing test

      Review the “Step 1: RED — Write a failing test” section in the pinned source before continuing.

      Review and apply the “Step 1: RED — Write a failing test” source section.
    3. 03

      Step 2: GREEN — Write minimal implementation

      Review the “Step 2: GREEN — Write minimal implementation” section in the pinned source before continuing.

      Review and apply the “Step 2: GREEN — Write minimal implementation” source section.
    4. 04

      Step 3: REFACTOR — Improve while tests stay green

      Review the “Step 3: REFACTOR — Improve while tests stay green” section in the pinned source before continuing.

      Review and apply the “Step 3: REFACTOR — Improve while tests stay green” source section.
    5. 05

      Fixtures and Setup/Teardown

      Place reusable helpers in t/lib/TestHelper.pm and load with use lib 't/lib'. Export factory functions like createtestdb(), createtempdir(), and fixturepath() via Exporter.

      Place reusable helpers in t/lib/TestHelper.pm and load with use lib 't/lib'. Export factory functions like createtestdb(), createtempdir(), and fixturepath() via Exporter.

    Permission review

    Static risk signals and limitations

    No configured static risk pattern was detected

    This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score91/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    skills/perl-testing/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    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.

    # Step 1: RED — Write a failing test
    # t/unit/calculator.t
    use 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.pm
    package 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

    use v5.36;
    use Test::More;
    
    # Plan upfront or use done_testing
    # plan tests => 5;  # Fixed plan (optional)
    
    # Equality
    is($result, 42, 'returns correct value');
    isnt($result, 0, 'not zero');
    
    # Boolean
    ok($user->is_active, 'user is active');
    ok(!$user->is_banned, 'user is not banned');
    
    # Deep comparison
    is_deeply(
        $got,
        { name => 'Alice', roles => ['admin'] },
        'returns expected structure'
    );
    
    # Pattern matching
    like($error, qr/not found/i, 'error mentions not found');
    unlike($output, qr/password/, 'output hides password');
    
    # Type check
    isa_ok($obj, 'MyApp::User');
    can_ok($obj, 'save', 'delete');
    
    done_testing;
    

    SKIP and TODO

    use v5.36;
    use Test::More;
    
    # Skip tests conditionally
    SKIP: {
        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 failures
    TODO: {
        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

    use v5.36;
    use Test2::V0;
    
    # Hash builder — check partial structure
    is(
        $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 builder
    is(
        $result,
        array {
            item 'first';
            item match(qr/^second/);
            item DNE();  # Does Not Exist — verify no extra items
        },
        'result matches expected list'
    );
    
    # Bag — order-independent comparison
    is(
        $tags,
        bag {
            item 'perl';
            item 'testing';
            item 'tdd';
        },
        'has all required tags regardless of order'
    );
    

    Subtests

    use 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

    use v5.36;
    use Test2::V0;
    
    # Test that code dies
    like(
        dies { divide(10, 0) },
        qr/Division by zero/,
        'dies on division by zero'
    );
    
    # Test that code lives
    ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
    
    # Combined pattern
    subtest '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

    t/
    ├── 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

    # Run all tests
    prove -l t/
    
    # Verbose output
    prove -lv t/
    
    # Run specific test
    prove -lv t/unit/user.t
    
    # Recursive search
    prove -lr t/
    
    # Parallel execution (8 jobs)
    prove -lr -j8 t/
    
    # Run only failing tests from last run
    prove -l --state=failed t/
    
    # Colored output with timer
    prove -l --color --timer t/
    
    # TAP output for CI
    prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
    

    .proverc Configuration

    -l
    --color
    --timer
    -r
    -j4
    --state=save
    

    Fixtures and Setup/Teardown

    Subtest Isolation

    use 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

    use 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

    # Basic coverage report
    cover -test
    
    # Or step by step
    perl -MDevel::Cover -Ilib t/unit/user.t
    cover
    
    # HTML report
    cover -report html
    open cover_db/coverage.html
    
    # Specific thresholds
    cover -test -report text | grep 'Total'
    
    # CI-friendly: fail under threshold
    cover -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.

    use 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

    TaskCommand / Pattern
    Run all testsprove -lr t/
    Run one test verboseprove -lv t/unit/user.t
    Parallel test runprove -lr -j8 t/
    Coverage reportcover -test && cover -report html
    Test equalityis($got, $expected, 'label')
    Deep comparisonis($got, hash { field k => 'v'; etc() }, 'label')
    Test exceptionlike(dies { ... }, qr/msg/, 'label')
    Test no exceptionok(lives { ... }, 'label')
    Mock a methodTest::MockModule->new('Pkg')->mock(m => sub { ... })
    Skip testsSKIP: { skip 'reason', $count unless $cond; ... }
    TODO testsTODO: { local $TODO = 'reason'; ... }

    Common Pitfalls

    Forgetting done_testing

    # Bad: Test file runs but doesn't verify all tests executed
    use Test2::V0;
    is(1, 1, 'works');
    # Missing done_testing — silent bugs if test code is skipped
    
    # Good: Always end with done_testing
    use Test2::V0;
    is(1, 1, 'works');
    done_testing;
    

    Missing -l Flag

    # Bad: Modules in lib/ not found
    prove t/unit/user.t
    # Can't locate MyApp/User.pm in @INC
    
    # Good: Include lib/ in @INC
    prove -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.

    Alternatives

    Compare before choosing