Install any skill in seconds. Free to start, no credit card required.
Get Started Free →日本語翻訳:このファイルは perl-testing 用の日本語翻訳が必要です
.claude/skills/affaan-m-perl-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 178% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 157% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 114% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 261% | 0% |
使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。
始终遵循 RED-GREEN-REFACTOR 循环。
perl# 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
标准的 Perl 测试模块 —— 广泛使用,随核心发行。
perluse 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;
perluse 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 是 Test::More 的现代替代品 —— 更丰富的断言、更好的诊断和可扩展性。
perluse 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' );
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;
perluse 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;
textt/ ├── 00-load.t # 验证模块编译 ├── 01-basic.t # 核心功能 ├── unit/ │ ├── config.t # 按模块划分的单元测试 │ ├── user.t │ └── util.t ├── integration/ │ ├── database.t │ └── api.t ├── lib/ │ └── TestHelper.pm # 共享测试工具 └── fixtures/ ├── config.json # 测试数据文件 └── users.csv
bash# 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
text-l --color --timer -r -j4 --state=save
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) };
将可重用的助手放在 t/lib/TestHelper.pm 中,并通过 use lib 't/lib' 加载。通过 Exporter 导出工厂函数,例如 create_test_db()、create_temp_dir() 和 fixture_path()。
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
对于轻量级的模拟对象,使用 Test::MockObject 创建可注入的测试替身,使用 ->mock() 并验证调用 ->called_ok()。
bash# 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 }'
对数据库测试使用内存中的 SQLite,对 API 测试模拟 HTTP::Tiny。
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;
prove -l:始终将 lib/ 包含在 @INC 中'user login with invalid password fails'done_testing:确保所有计划的测试都已运行Test::More:首选 Test2::V0| 任务 | 命令 / 模式 | |---|---| | 运行所有测试 | prove -lr t/ | | 详细运行单个测试 | prove -lv t/unit/user.t | | 并行测试运行 | prove -lr -j8 t/ | | 覆盖率报告 | cover -test && cover -report html | | 测试相等性 | is($got, $expected, 'label') | | 深层比较 | is($got, hash { field k => 'v'; etc() }, 'label') | | 测试异常 | like(dies { ... }, qr/msg/, 'label') | | 测试无异常 | ok(lives { ... }, 'label') | | 模拟一个方法 | Test::MockModule->new('Pkg')->mock(m => sub { ... }) | | 跳过测试 | SKIP: { skip 'reason', $count unless $cond; ... } | | TODO 测试 | TODO: { local $TODO = 'reason'; ... } |
done_testingperl# 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;
-l 标志bash# 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
模拟依赖项,而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容,那么它什么也没测试。
在子测试内部使用 my 变量 —— 永远不要用 our —— 以防止状态在测试之间泄漏。
记住:测试是你的安全网。保持它们快速、专注和独立。新项目使用 Test2::V0,运行使用 prove,问责使用 Devel::Cover。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→pass | 10,297 | 6,684 | -35% | 1 | 1 | 0% | 1,646 | 4,578 | +178% | 0 | 0 | — |
case-01 | pass→pass | 15,266 | 8,916 | -42% | 1 | 1 | 0% | 3,009 | 5,109 | +70% | 0 | 0 | — |
case-02 | fail→pass | 9,180 | 4,769 | -48% | 1 | 1 | 0% | 1,681 | 4,318 | +157% | 0 | 0 | — |
case-03 | fail→pass | 14,298 | 7,766 | -46% | 1 | 1 | 0% | 2,508 | 4,862 | +94% | 0 | 0 | — |
case-04 | pass→pass | 9,635 | 5,113 | -47% | 1 | 1 | 0% | 1,676 | 4,334 | +159% | 0 | 0 | — |
case-06 | fail→pass | 13,200 | 10,926 | -17% | 1 | 1 | 0% | 2,573 | 5,518 | +114% | 0 | 0 | — |
case-07 | pass→pass | 14,421 | 9,045 | -37% | 1 | 1 | 0% | 2,005 | 5,138 | +156% | 0 | 0 | — |
case-08 | pass→pass | 16,563 | 11,593 | -30% | 1 | 1 | 0% | 2,265 | 5,376 | +137% | 0 | 0 | — |
case-09 | fail→pass | 6,379 | 3,698 | -42% | 1 | 1 | 0% | 1,115 | 4,021 | +261% | 0 | 0 | — |
case-15 | fail→pass | 23,477 | 11,574 | -51% | 1 | 1 | 0% | 4,198 | 5,599 | +33% | 0 | 0 | — |
case-10 | pass→pass | 6,047 | 4,104 | -32% | 1 | 1 | 0% | 872 | 4,019 | +361% | 0 | 0 | — |
case-11 | fail→fail | 9,888 | 11,569 | +17% | 1 | 1 | 0% | 1,625 | 5,380 | +231% | 0 | 0 | — |
case-12 | pass→pass | 6,020 | 7,599 | +26% | 1 | 1 | 0% | 975 | 4,672 | +379% | 0 | 0 | — |
case-13 | pass→pass | 9,945 | 9,082 | -9% | 1 | 1 | 0% | 1,693 | 5,059 | +199% | 0 | 0 | — |
case-14 | pass→pass | 8,896 | 14,605 | +64% | 1 | 1 | 0% | 1,498 | 5,178 | +246% | 0 | 0 | — |
case-16 | pass→pass | 13,023 | 7,026 | -46% | 1 | 1 | 0% | 2,119 | 4,583 | +116% | 0 | 0 | — |
case-17 | pass→pass | 3,928 | 5,017 | +28% | 1 | 1 | 0% | 666 | 4,178 | +527% | 0 | 0 | — |
case-18 | pass→pass | 11,707 | 6,399 | -45% | 1 | 1 | 0% | 1,822 | 4,565 | +151% | 0 | 0 | — |
case-19 | pass→pass | 4,093 | 2,996 | -27% | 1 | 1 | 0% | 589 | 3,819 | +548% | 0 | 0 | — |
case-20 | pass→pass | 6,124 | 4,334 | -29% | 1 | 1 | 0% | 1,136 | 4,196 | +269% | 0 | 0 | — |
case-21 | pass→pass | 12,229 | 11,559 | -5% | 1 | 1 | 0% | 2,347 | 5,599 | +139% | 0 | 0 | — |
case-22 | pass→pass | 6,668 | 7,263 | +9% | 1 | 1 | 0% | 1,334 | 4,786 | +259% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.