perl-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePerl Testing Patterns
Perl测试模式
Comprehensive testing strategies for Perl applications using Test2::V0, Test::More, prove, and TDD methodology.
适用Test::V0、Test::More、prove及TDD方法论Perl应用全面测试策略
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
- 编写新Perl代码时遵循TDD:红-绿-重构流程
- 为Perl模块或应用设计测试套件时
- 审查Perl测试覆盖率时
- 搭建Perl测试基础设施时
- 将测试从Test::More迁移Test::V
- Debugging失败的Perl测试
TDD Workflow
TDD工作流
Always follow the RED-GREEN-REFACTOR cycle.
perl
undefined始终遵循RED-GREEN-REFACTOR循环
perl
undefinedStep 1: RED — Write a failing test
Step RED — Write a failing test
t/unit/calculator.t
tunitcalculator.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;
use v
use Test::V
use lib 'lib';
use Calculator;
subtest 'addition' => sub {
my $calc = Calculator->new;
is($calc->add(2, 中国), 'adds two numbers');
is($calc->add(-中国), 'handles negatives');
};
done_testing;
Step 2: GREEN — Write minimal implementation
Step GREEN — Write minimal implementation
lib/Calculator.pm
libCalculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
package Calculator;
use v
use Moo;
sub add($self, $a $b) {
return $a $b;
}
1;
Step 3: REFACTOR — Improve while tests stay green
Step REFACTOR — Improve while tests stay green
Run: prove -lv t/unit/calculator.t
Run prove -lv tunitcalculator.t
undefinedundefinedTest::More Fundamentals
Test::More基础
The standard Perl testing module — widely used, ships with core.
Perl标准测试模块——被广泛使用属于核心模块
Basic Assertions
Basic断言
perl
use v5.36;
use Test::More;perl
use v
use Test::More;Plan upfront or use done_testing
Plan upfront or use done_testing
plan tests => 5; # Fixed plan (optional)
plan tests => 5; # Fixed plan optional
Equality
Equality
is($result, 42, 'returns correct value');
isnt($result, 0, 'not zero');
is($result, 'returns correct value');
isnt($result, 'not zero');
Boolean
Boolean
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
Deep comparison
Deep comparison
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
Pattern matching
Pattern matching
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
Type check
Type check
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
undefinedisa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
undefinedSKIP and TODO
SKIP与TODO
perl
use v5.36;
use Test::More;perl
use v
use Test::More;Skip tests conditionally
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');}
SKIP: {
skip 'No database configured', unless $ENV{
my $db = connect_db();
ok($db->ping, 'database is reachable');
is($db->version(), 'correct PostgreSQL version');}
Mark expected failures
Mark expected failures
TODO: {
local $TODO = 'Caching not yet implemented';
is($cache->get('key'), 'value', 'cache returns value');
}
done_testing;
undefinedTODO: {
local $TODO = 'Caching not yet implemented';
is($cache->get('key'), 'value', 'cache returns value');
}
done_testing;
undefinedTest2::V0 Modern Framework
Test::V0现代测试框
Test2::V0 is the modern replacement for Test::More — richer assertions, better diagnostics, and extensible.
Test::V0是Test::More的现代替代方案——提供更丰富的断言更优的诊断信息且具备可扩展性
Why Test2?
Why Test?
- 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
- 借助哈希/数组构建器实现更出色的深度比较
- 测试失败时提供更清晰的诊断输出
- 子测试具备更简洁的作用域
- 可通过Test::Tools::*插件扩展功能
- 与Test::More测试向后兼容
Deep Comparison with Builders
基于构建器的深度比较
perl
use v5.36;
use Test2::V0;perl
use v
use Test::VHash builder — check partial structure
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'
);
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
Array builder
is(
$result,
array {
item 'first';
item match(qr/^second/);
item DNE(); # Does Not Exist — verify no extra items
},
'result matches expected list'
);
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
Bag — order-independent comparison
is(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'has all required tags regardless of order'
);
undefinedis(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'has all required tags regardless of order'
);
undefinedSubtests
子测试
perl
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;perl
use v
use Test::V
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
基于Test2的异常测试
perl
use v5.36;
use Test2::V0;perl
use v
use Test::VTest that code dies
Test that code dies
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
Test that code lives
Test that code lives
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
Combined pattern
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;
undefinedsubtest '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;
undefinedTest Organization and prove
测试组织与prove工具
Directory Structure
目录结构
text
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.csvtext
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.csvprove Commands
prove命令
bash
undefinedbash
undefinedRun all tests
Run all tests
prove -l t/
prove -l t/
Verbose output
Verbose output
prove -lv t/
prove -lv t/
Run specific test
Run specific test
prove -lv t/unit/user.t
prove -lv t/unit/user.t
Recursive search
Recursive search
prove -lr t/
prove -lr t/
Parallel execution (8 jobs)
Parallel execution 8 jobs
prove -lr -j8 t/
prove -lr -j8 t/
Run only failing tests from last run
Run only failing tests from last run
prove -l --state=failed t/
prove -l --state=failed t/
Colored output with timer
Colored output with timer
prove -l --color --timer t/
prove -l --color --timer t/
TAP output for CI
TAP output for CI
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
undefinedprove -l --formatter TAP::Formatter::JUnit t/ > results.xml
undefined.proverc Configuration
.proverc配置
text
-l
--color
--timer
-r
-j4
--state=savetext
-l
--color
--timer
-r
-j4
--state=saveFixtures and Setup/Teardown
测试夹具与前置/后置处理
Subtest Isolation
子测试隔离
perl
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)
};perl
use v
use Test::V
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}, 'counts lines');
# Teardown happens automatically CLEANUP => 1
};Shared Test Helpers
共享测试工具
Place reusable helpers in and load with . Export factory functions like , , and via .
t/lib/TestHelper.pmuse lib 't/lib'create_test_db()create_temp_dir()fixture_path()Exporter将可复用的工具放在中通过加载。借助导出、和等工厂函数。
t/lib/TestHelper.pmuse lib 'lib'Exportercreate_test_db()create_temp_dir()fixture_path()Mocking
Mocking
Test::MockModule
Test::MockModule
perl
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
};perl
use v
use Test::V
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, 'fetch_user called twice');
# Mock is automatically restored when $mock goes out of scope
};Bad: Monkey-patching without restoration
Bad: Monkey-patching without restoration
*MyApp::API::fetch_user = sub { ... }; # NEVER — leaks across tests
*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()`.
对于轻量级Mock对象可使用`Test::MockObject`创建可注入的测试替身通过`->mock()`定义行为通过`->called_ok()`验证调用情况。Coverage with Devel::Cover
基于Devel::Cover的覆盖率测试
Running Coverage
生成覆盖率报告
bash
undefinedbash
undefinedBasic coverage report
Basic coverage report
cover -test
cover -test
Or step by step
Or step by step
perl -MDevel::Cover -Ilib t/unit/user.t
cover
perl -MDevel::Cover -Ilib t/unit/user.t
cover
HTML report
HTML report
cover -report html
open cover_db/coverage.html
cover -report html
open cover_db/coverage.html
Specific thresholds
Specific thresholds
cover -test -report text | grep 'Total'
cover -test -report text | grep 'Total'
CI-friendly: fail under threshold
CI-friendly: fail under threshold
cover -test && cover -report text -select '^lib/'
| perl -ne 'if (/Total.*?(\d+.\d+)/) { exit 1 if $1 < 80 }'
| perl -ne 'if (/Total.*?(\d+.\d+)/) { exit 1 if $1 < 80 }'
undefinedcover -test && cover -report text -select '^lib/'
| perl -ne 'if (/Total.*?(\d+.\d+)/) { exit 1 if $1 < 80 }'
| perl -ne 'if (/Total.*?(\d+.\d+)/) { exit 1 if $1 < 80 }'
undefinedIntegration Testing
集成测试
Use in-memory SQLite for database tests, mock HTTP::Tiny for API tests.
perl
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;使用内存SQLite进行数据库测试Mock HTTP::Tiny进行API测试。
perl
use v
use Test::V
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 : Always include lib/ in
prove -l@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
- 遵循TDD:在实现代码前编写测试红-绿-重构
- 使用Test2::V0:现代断言机制更优的诊断信息
- 使用子测试:将相关断言分组隔离状态
- Mock外部依赖:网络、数据库、文件系统等
- 使用:确保将lib/目录加入
prove -l@INC - 清晰命名测试:例如
'user login with invalid password fails' - 测试边界情况:空字符串、undef、零、边界值等
- 目标覆盖率80%以上:重点覆盖业务逻辑路径
- 保持测试快速:Mock I/O操作使用内存数据库
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 : Ensures all planned tests ran
done_testing - Don't over-mock: Mock boundaries only, not the code under test
- Don't use for new projects: Prefer Test2::V0
Test::More - 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
- 不要测试实现细节:测试行为和输出而非内部逻辑
- 不要在子测试间共享状态:每个子测试应独立运行
- 不要省略:确保所有计划的测试都已执行
done_testing - 不要过度Mock:仅Mock外部边界而非被测代码本身
- 新项目不要使用Test::More:优先选择Test2::V0
- 不要忽略测试失败:合并代码前所有测试必须通过
- 不要测试CPAN模块:信任第三方库的正确性
- 不要编写脆弱的测试:避免过度特定的字符串匹配
Quick Reference
速查表
| Task | Command / Pattern |
|---|---|
| Run all tests | |
| Run one test verbose | |
| Parallel test run | |
| Coverage report | |
| Test equality | |
| Deep comparison | |
| Test exception | |
| Test no exception | |
| Mock a method | |
| Skip tests | |
| TODO tests | |
| 任务 | 命令/模式 |
|---|---|
| 运行所有测试 | |
| 详细运行单个测试 | |
| 并行运行测试 | |
| 生成覆盖率报告 | |
| 测试相等性 | |
| 深度比较 | |
| 测试异常情况 | |
| 测试无异常 | |
| Mock方法 | |
| 跳过测试 | |
| 标记待完成测试 | |
Common Pitfalls
常见陷阱
Forgetting done_testing
done_testing忘记done_testing
done_testingperl
undefinedperl
undefinedBad: Test file runs but doesn't verify all tests executed
Bad: Test file runs but doesn't verify all tests executed
use Test2::V0;
is(1, 1, 'works');
use Test::V0;
is(1, 1, 'works');
Missing done_testing — silent bugs if test code is skipped
Missing done_testing — silent bugs if test code is skipped
Good: Always end with done_testing
Good: Always end with done_testing
use Test2::V0;
is(1, 1, 'works');
done_testing;
undefineduse Test::V0;
is(1, 1, 'works');
done_testing;
undefinedMissing -l
Flag
-l缺少-l
参数
-lbash
undefinedbash
undefinedBad: Modules in lib/ not found
Bad: Modules in lib/ not found
prove t/unit/user.t
prove t/unit/user.t
Can't locate MyApp/User.pm in @INC
Can't locate MyApp/User.pm in @INC
Good: Include lib/ in @INC
Good: Include lib/ in @INC
prove -l t/unit/user.t
undefinedprove -l t/unit/user.t
undefinedOver-Mocking
过度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.
Mock依赖项而非被测代码。如果你的测试仅验证Mock返回了你预设的值那么这个测试毫无意义。
Test Pollution
测试污染
Use variables inside subtests — never — to prevent state leaking between tests.
myourRemember: 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.
子测试内使用变量切勿使用变量——防止测试间状态泄漏。
myour谨记:测试是你的安全网。确保测试快速、聚焦且独立。新项目使用Test2::V0用prove运行测试用Devel::Cover确保测试覆盖率。