perl-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Perl 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
undefined

Step 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

undefined
undefined

Test::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;
undefined
isa_ok($obj, 'MyApp::User'); can_ok($obj, 'save', 'delete');
done_testing;
undefined

SKIP 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;
undefined
TODO: { local $TODO = 'Caching not yet implemented'; is($cache->get('key'), 'value', 'cache returns value'); }
done_testing;
undefined

Test2::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::V

Hash 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' );
undefined
is( $tags, bag { item 'perl'; item 'testing'; item 'tdd'; }, 'has all required tags regardless of order' );
undefined

Subtests

子测试

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::V

Test 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;
undefined
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;
undefined

Test 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.csv
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.csv

prove Commands

prove命令

bash
undefined
bash
undefined

Run 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
undefined
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
undefined

.proverc Configuration

.proverc配置

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

Fixtures 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
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
.
将可复用的工具放在
t/lib/TestHelper.pm
中通过
use lib 'lib'
加载。借助
Exporter
导出
create_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
undefined
bash
undefined

Basic 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 }'
undefined
cover -test && cover -report text -select '^lib/'
| perl -ne 'if (/Total.*?(\d+.\d+)/) { exit 1 if $1 < 80 }'
undefined

Integration 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
    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
  • 遵循TDD:在实现代码前编写测试红-绿-重构
  • 使用Test2::V0:现代断言机制更优的诊断信息
  • 使用子测试:将相关断言分组隔离状态
  • Mock外部依赖:网络、数据库、文件系统等
  • 使用
    prove -l
    :确保将lib/目录加入
    @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
    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
  • 不要测试实现细节:测试行为和输出而非内部逻辑
  • 不要在子测试间共享状态:每个子测试应独立运行
  • 不要省略
    done_testing
    :确保所有计划的测试都已执行
  • 不要过度Mock:仅Mock外部边界而非被测代码本身
  • 新项目不要使用Test::More:优先选择Test2::V0
  • 不要忽略测试失败:合并代码前所有测试必须通过
  • 不要测试CPAN模块:信任第三方库的正确性
  • 不要编写脆弱的测试:避免过度特定的字符串匹配

Quick Reference

速查表

TaskCommand / 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')
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'; ... }
任务命令/模式
运行所有测试
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')
Mock方法
Test::MockModule->new('Pkg')->mock(m => sub { ... })
跳过测试
SKIP: { skip 'reason', $count unless $cond; ... }
标记待完成测试
TODO: { local $TODO = 'reason'; ... }

Common Pitfalls

常见陷阱

Forgetting
done_testing

忘记
done_testing

perl
undefined
perl
undefined

Bad: 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;
undefined
use Test::V0; is(1, 1, 'works'); done_testing;
undefined

Missing
-l
Flag

缺少
-l
参数

bash
undefined
bash
undefined

Bad: 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
undefined
prove -l t/unit/user.t
undefined

Over-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
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.
子测试内使用
my
变量切勿使用
our
变量——防止测试间状态泄漏。
谨记:测试是你的安全网。确保测试快速、聚焦且独立。新项目使用Test2::V0用prove运行测试用Devel::Cover确保测试覆盖率。