perl-testing by affaan-m/everything-claude-code
npx skills add https://github.com/affaan-m/everything-claude-code --skill perl-testing使用 Test2::V0、Test::More、prove 和 TDD 方法为 Perl 应用程序制定全面的测试策略。
始终遵循红-绿-重构循环。
# 步骤 1: 红 — 编写一个失败的测试
# 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;
# 步骤 2: 绿 — 编写最小实现
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# 步骤 3: 重构 — 在测试保持通过的情况下改进
# 运行: prove -lv t/unit/calculator.t
标准的 Perl 测试模块 — 广泛使用,随核心发行。
use v5.36;
use Test::More;
# 提前计划或使用 done_testing
# plan tests => 5; # 固定计划(可选)
# 相等性
is($result, 42, 'returns correct value');
isnt($result, 0, 'not zero');
# 布尔值
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
# 深度比较
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
# 模式匹配
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
# 类型检查
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
use v5.36;
use Test::More;
# 有条件地跳过测试
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');
}
# 标记预期失败
TODO: {
local $TODO = 'Caching not yet implemented';
is($cache->get('key'), 'value', 'cache returns value');
}
done_testing;
Test2::V0 是 Test::More 的现代替代品 — 更丰富的断言、更好的诊断和可扩展性。
use v5.36;
use Test2::V0;
# 哈希构建器 — 检查部分结构
is(
$user->to_hash,
hash {
field name => 'Alice';
field email => match(qr/\@example\.com$/);
field age => validator(sub { $_ >= 18 });
# 忽略其他字段
etc();
},
'user has expected fields'
);
# 数组构建器
is(
$result,
array {
item 'first';
item match(qr/^second/);
item DNE(); # 不存在 — 验证没有额外项
},
'result matches expected list'
);
# 包 — 顺序无关的比较
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;
use v5.36;
use Test2::V0;
# 测试代码是否抛出异常
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
# 测试代码是否正常执行
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
# 组合模式
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;
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 -l t/
# 详细输出
prove -lv t/
# 运行特定测试
prove -lv t/unit/user.t
# 递归搜索
prove -lr t/
# 并行执行(8 个任务)
prove -lr -j8 t/
# 仅运行上次失败的测试
prove -l --state=failed t/
# 带计时器的彩色输出
prove -l --color --timer t/
# 用于 CI 的 TAP 输出
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
-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 {
# 设置
my $dir = tempdir(CLEANUP => 1);
my $file = path($dir, 'input.txt');
$file->spew_utf8("line1\nline2\nline3\n");
# 测试
my $result = process_file("$file");
is($result->{line_count}, 3, 'counts lines');
# 清理自动发生 (CLEANUP => 1)
};
将可重用的助手放在 t/lib/TestHelper.pm 中,并通过 use lib 't/lib' 加载。通过 Exporter 导出工厂函数,如 create_test_db()、create_temp_dir() 和 fixture_path()。
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'mock external API' => sub {
my $mock = Test::MockModule->new('MyApp::API');
# 良好:模拟返回受控数据
$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');
# 验证调用次数
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 超出作用域时,模拟会自动恢复
};
# 不良:未经恢复的猴子补丁
# *MyApp::API::fetch_user = sub { ... }; # 绝对不要 — 会在测试间泄漏
对于轻量级的模拟对象,使用 Test::MockObject 来创建可注入的测试替身,使用 ->mock() 并通过 ->called_ok() 验证调用。
# 基本覆盖率报告
cover -test
# 或分步进行
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# HTML 报告
cover -report html
open cover_db/coverage.html
# 特定阈值
cover -test -report text | grep 'Total'
# 适合 CI:低于阈值时失败
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;
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_testing# 不良:测试文件运行但不验证所有测试是否执行
use Test2::V0;
is(1, 1, 'works');
# 缺少 done_testing — 如果测试代码被跳过,会出现静默错误
# 良好:始终以 done_testing 结尾
use Test2::V0;
is(1, 1, 'works');
done_testing;
-l 标志# 不良:未找到 lib/ 中的模块
prove t/unit/user.t
# Can't locate MyApp/User.pm in @INC
# 良好:将 lib/ 包含在 @INC 中
prove -l t/unit/user.t
模拟 依赖项 ,而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容,那么它什么也没测试。
在子测试内部使用 my 变量 — 永远不要用 our — 以防止状态在测试间泄漏。
记住 : 测试是你的安全网。保持它们快速、专注和独立。对新项目使用 Test2::V0,使用 prove 运行,使用 Devel::Cover 确保责任。
每周安装数
33
仓库
GitHub 星标数
72.1K
首次出现
1 天前
安全审计
安装于
codex30
github-copilot28
kimi-cli28
amp28
cline28
gemini-cli28
Comprehensive testing strategies for Perl applications using Test2::V0, Test::More, prove, and TDD methodology.
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
The standard Perl testing module — widely used, ships with core.
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;
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 is the modern replacement for Test::More — richer assertions, better diagnostics, and extensible.
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;
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;
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
# 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
-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)
};
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.
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().
# 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 }'
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;
prove -l: Always include lib/ in @INC'user login with invalid password fails'done_testing: Ensures all planned tests ranTest::More for new projects: Prefer Test2::V0| 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') |
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 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
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.
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.
Weekly Installs
33
Repository
GitHub Stars
72.1K
First Seen
1 day ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex30
github-copilot28
kimi-cli28
amp28
cline28
gemini-cli28
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
105,000 周安装
| 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'; ... } |