Perl Testing

affaan-m/ECC/docs/zh-CN/skills/perl-testing

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775无许可证275K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库4天前更新

使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。

AI 生成的概览

指导 Perl 测试,涵盖 Test2::V0、Test::More、prove、模拟、Devel::Cover 覆盖率与 TDD。

功能
该技能提供 Perl 测试模式与规范,涵盖 TDD 工作流、Test::More 基础、Test2::V0 断言与构建器、子测试组织、夹具、模拟以及 Devel::Cover 覆盖率分析。它产出的是指导说明和代码示例,而非可执行产物,并包含快速参考表和常见陷阱。它仅包含说明,不附带脚本。
适用场景
适用于以测试先行方式编写新的 Perl 代码、为 Perl 模块或应用设计测试套件、审查 Perl 测试覆盖率、搭建 Perl 测试基础设施、从 Test::More 迁移到 Test2::V0,或调试失败的 Perl 测试。
运行要求
不包含脚本,仅为说明文档。按示例操作需要 Perl 运行时以及 Test2::V0、Test::More、prove,可选使用 Test::MockModule、Test::MockObject、Devel::Cover、DBI、File::Temp 和 Path::Tiny。

Perl 测试模式

使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。

何时激活

  • 编写新的 Perl 代码(遵循 TDD:红、绿、重构)
  • 为 Perl 模块或应用程序设计测试套件
  • 审查 Perl 测试覆盖率
  • 设置 Perl 测试基础设施
  • 将测试从 Test::More 迁移到 Test2::V0
  • 调试失败的 Perl 测试

TDD 工作流程

始终遵循 RED-GREEN-REFACTOR 循环。

perl
# Step 1: RED — Write a failing test# t/unit/calculator.tuse 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.pmpackage 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 测试模块 —— 广泛使用,随核心发行。

基本断言

perl
use v5.36;use Test::More;
# Plan upfront or use done_testing# plan tests => 5;  # Fixed plan (optional)
# Equalityis($result, 42, 'returns correct value');isnt($result, 0, 'not zero');
# Booleanok($user->is_active, 'user is active');ok(!$user->is_banned, 'user is not banned');
# Deep comparisonis_deeply(    $got,    { name => 'Alice', roles => ['admin'] },    'returns expected structure');
# Pattern matchinglike($error, qr/not found/i, 'error mentions not found');unlike($output, qr/password/, 'output hides password');
# Type checkisa_ok($obj, 'MyApp::User');can_ok($obj, 'save', 'delete');
done_testing;

SKIP 和 TODO

perl
use v5.36;use Test::More;
# Skip tests conditionallySKIP: {    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 failuresTODO: {    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 测试向后兼容

使用构建器进行深层比较

perl
use v5.36;use Test2::V0;
# Hash builder — check partial structureis(    $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 builderis(    $result,    array {        item 'first';        item match(qr/^second/);        item DNE();  # Does Not Exist — verify no extra items    },    'result matches expected list');
# Bag — order-independent comparisonis(    $tags,    bag {        item 'perl';        item 'testing';        item 'tdd';    },    'has all required tags regardless of order');

子测试

perl
use v5.36;use Test2::V0;
subtest 'User creation' => sub {    my $user = User->new(name => 'Alice', email => '[email protected]');    ok($user, 'user object created');    is($user->name, 'Alice', 'name is set');    is($user->email, '[email protected]', 'email is set');};
subtest 'User validation' => sub {    my $warnings = warns {        User->new(name => '', email => 'bad');    };    ok($warnings, 'warns on invalid data');};
done_testing;

使用 Test2 进行异常测试

perl
use v5.36;use Test2::V0;
# Test that code dieslike(    dies { divide(10, 0) },    qr/Division by zero/,    'dies on division by zero');
# Test that code livesok(lives { divide(10, 2) }, 'division succeeds') or note($@);
# Combined patternsubtest '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

目录结构

text
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 命令

bash
# Run all testsprove -l t/
# Verbose outputprove -lv t/
# Run specific testprove -lv t/unit/user.t
# Recursive searchprove -lr t/
# Parallel execution (8 jobs)prove -lr -j8 t/
# Run only failing tests from last runprove -l --state=failed t/
# Colored output with timerprove -l --color --timer t/
# TAP output for CIprove -l --formatter TAP::Formatter::JUnit t/ > results.xml

.proverc 配置

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

夹具与设置/拆卸

子测试隔离

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)};

共享测试助手

将可重用的助手放在 t/lib/TestHelper.pm 中,并通过 use lib 't/lib' 加载。通过 Exporter 导出工厂函数,例如 create_test_db()、create_temp_dir() 和 fixture_path()。

模拟

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 => '[email protected]' };    });
    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 进行覆盖率分析

运行覆盖率分析

bash
# Basic coverage reportcover -test
# Or step by stepperl -MDevel::Cover -Ilib t/unit/user.tcover
# HTML reportcover -report htmlopen cover_db/coverage.html
# Specific thresholdscover -test -report text | grep 'Total'
# CI-friendly: fail under thresholdcover -test && cover -report text -select '^lib/' \  | perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'

集成测试

对数据库测试使用内存中的 SQLite,对 API 测试模拟 HTTP::Tiny。

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;

最佳实践

应做事项

  • 遵循 TDD:在实现之前编写测试(红-绿-重构)
  • 使用 Test2::V0:现代断言,更好的诊断
  • 使用子测试:分组相关断言,隔离状态
  • 模拟外部依赖:网络、数据库、文件系统
  • 使用 prove -l:始终将 lib/ 包含在 @INC 中
  • 清晰命名测试:'user login with invalid password fails'
  • 测试边界情况:空字符串、undef、零、边界值
  • 目标 80%+ 覆盖率:专注于业务逻辑路径
  • 保持测试快速:模拟 I/O,使用内存数据库

禁止事项

  • 不要测试实现:测试行为和输出,而非内部细节
  • 不要在子测试之间共享状态:每个子测试都应是独立的
  • 不要跳过 done_testing:确保所有计划的测试都已运行
  • 不要过度模拟:仅模拟边界,而非被测试的代码
  • 不要在新项目中使用 Test::More:首选 Test2::V0
  • 不要忽略测试失败:所有测试必须在合并前通过
  • 不要测试 CPAN 模块:相信库能正常工作
  • 不要编写脆弱的测试:避免过度具体的字符串匹配

快速参考

任务命令 / 模式
运行所有测试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

perl
# Bad: Test file runs but doesn't verify all tests executeduse Test2::V0;is(1, 1, 'works');# Missing done_testing — silent bugs if test code is skipped
# Good: Always end with done_testinguse Test2::V0;is(1, 1, 'works');done_testing;

缺少 -l 标志

bash
# Bad: Modules in lib/ not foundprove t/unit/user.t# Can't locate MyApp/User.pm in @INC
# Good: Include lib/ in @INCprove -l t/unit/user.t

过度模拟

模拟依赖项,而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容,那么它什么也没测试。

测试污染

在子测试内部使用 my 变量 —— 永远不要用 our —— 以防止状态在测试之间泄漏。

记住:测试是你的安全网。保持它们快速、专注和独立。新项目使用 Test2::V0,运行使用 prove,问责使用 Devel::Cover。

来源与署名

来源:affaan-m/ECC位于docs/zh-CN/skills/perl-testing提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架