Everything-claude-code perl-testing
使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。
install
source · Clone the upstream repo
git clone https://github.com/affaan-m/everything-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/affaan-m/everything-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/zh-CN/skills/perl-testing" ~/.claude/skills/affaan-m-everything-claude-code-perl-testing && rm -rf "$T"
manifest:
docs/zh-CN/skills/perl-testing/SKILL.mdsource content
Perl 测试模式
使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。
何时激活
- 编写新的 Perl 代码(遵循 TDD:红、绿、重构)
- 为 Perl 模块或应用程序设计测试套件
- 审查 Perl 测试覆盖率
- 设置 Perl 测试基础设施
- 将测试从 Test::More 迁移到 Test2::V0
- 调试失败的 Perl 测试
TDD 工作流程
始终遵循 RED-GREEN-REFACTOR 循环。
# 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 基础
标准的 Perl 测试模块 —— 广泛使用,随核心发行。
基本断言
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 和 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 现代框架
Test2::V0 是 Test::More 的现代替代品 —— 更丰富的断言、更好的诊断和可扩展性。
为什么选择 Test2?
- 使用哈希/数组构建器进行卓越的深层比较
- 失败时提供更好的诊断输出
- 具有更清晰作用域的子测试
- 可通过 Test2::Tools::* 插件扩展
- 与 Test::More 测试向后兼容
使用构建器进行深层比较
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' );
子测试
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;
使用 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;
测试组织与 prove
目录结构
t/ ├── 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
prove 命令
# 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 配置
-l --color --timer -r -j4 --state=save
夹具与设置/拆卸
子测试隔离
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) };
共享测试助手
将可重用的助手放在
t/lib/TestHelper.pm 中,并通过 use lib 't/lib' 加载。通过 Exporter 导出工厂函数,例如 create_test_db()、create_temp_dir() 和 fixture_path()。
模拟
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
对于轻量级的模拟对象,使用
Test::MockObject 创建可注入的测试替身,使用 ->mock() 并验证调用 ->called_ok()。
使用 Devel::Cover 进行覆盖率分析
运行覆盖率分析
# 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。
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;
最佳实践
应做事项
- 遵循 TDD:在实现之前编写测试(红-绿-重构)
- 使用 Test2::V0:现代断言,更好的诊断
- 使用子测试:分组相关断言,隔离状态
- 模拟外部依赖:网络、数据库、文件系统
- 使用
:始终将 lib/ 包含在prove -l
中@INC - 清晰命名测试:
'user login with invalid password fails' - 测试边界情况:空字符串、undef、零、边界值
- 目标 80%+ 覆盖率:专注于业务逻辑路径
- 保持测试快速:模拟 I/O,使用内存数据库
禁止事项
- 不要测试实现:测试行为和输出,而非内部细节
- 不要在子测试之间共享状态:每个子测试都应是独立的
- 不要跳过
:确保所有计划的测试都已运行done_testing - 不要过度模拟:仅模拟边界,而非被测试的代码
- 不要在新项目中使用
:首选 Test2::V0Test::More - 不要忽略测试失败:所有测试必须在合并前通过
- 不要测试 CPAN 模块:相信库能正常工作
- 不要编写脆弱的测试:避免过度具体的字符串匹配
快速参考
| 任务 | 命令 / 模式 |
|---|---|
| 运行所有测试 | |
| 详细运行单个测试 | |
| 并行测试运行 | |
| 覆盖率报告 | |
| 测试相等性 | |
| 深层比较 | |
| 测试异常 | |
| 测试无异常 | |
| 模拟一个方法 | |
| 跳过测试 | |
| TODO 测试 | |
常见陷阱
忘记 done_testing
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;
缺少 -l
标志
-l# 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。