[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6E2P2MT8RQJDPH5BCMF58YG":3,"skill-same:01M6E2P2MT8RQJDPH5BCMF58YG":61},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":28,"linkOnly":29,"markdownContent":30,"markdownBody":31,"frontMatter":32,"refName":34,"commitSha":35,"skillDirectory":36,"forksCount":13,"repositoryHtmlUrl":37,"repositoryHomepageUrl":38,"ownerHtmlUrl":39,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":40,"install":41,"resources":54},"01M6E2P2MT8RQJDPH5BCMF58YG","perl-testing",null,"使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。","7dvq6yrkgs-stack\u002Fplaywright","7dvq6yrkgs-stack","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F251638143?v=4","01M6E061J2AMGWYZ6TK7Y8NM74","01M6E061PQCQJGQMPBERMCFN5G",0,"Apache-2.0","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fperl-testing\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[23,24,25,26,27],"testing","perl testing","test","v0","runner","2026-11-10 10:12:30",false,"---\nname: perl-testing\ndescription: 使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。\norigin: ECC\n---\n\n# Perl 测试模式\n\n使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。\n\n## 何时激活\n\n* 编写新的 Perl 代码（遵循 TDD：红、绿、重构）\n* 为 Perl 模块或应用程序设计测试套件\n* 审查 Perl 测试覆盖率\n* 设置 Perl 测试基础设施\n* 将测试从 Test::More 迁移到 Test2::V0\n* 调试失败的 Perl 测试\n\n## TDD 工作流程\n\n始终遵循 RED-GREEN-REFACTOR 循环。\n\n```perl\n# Step 1: RED — Write a failing test\n# t\u002Funit\u002Fcalculator.t\nuse v5.36;\nuse Test2::V0;\n\nuse lib 'lib';\nuse Calculator;\n\nsubtest 'addition' => sub {\n    my $calc = Calculator->new;\n    is($calc->add(2, 3), 5, 'adds two numbers');\n    is($calc->add(-1, 1), 0, 'handles negatives');\n};\n\ndone_testing;\n\n# Step 2: GREEN — Write minimal implementation\n# lib\u002FCalculator.pm\npackage Calculator;\nuse v5.36;\nuse Moo;\n\nsub add($self, $a, $b) {\n    return $a + $b;\n}\n\n1;\n\n# Step 3: REFACTOR — Improve while tests stay green\n# Run: prove -lv t\u002Funit\u002Fcalculator.t\n```\n\n## Test::More 基础\n\n标准的 Perl 测试模块 —— 广泛使用，随核心发行。\n\n### 基本断言\n\n```perl\nuse v5.36;\nuse Test::More;\n\n# Plan upfront or use done_testing\n# plan tests => 5;  # Fixed plan (optional)\n\n# Equality\nis($result, 42, 'returns correct value');\nisnt($result, 0, 'not zero');\n\n# Boolean\nok($user->is_active, 'user is active');\nok(!$user->is_banned, 'user is not banned');\n\n# Deep comparison\nis_deeply(\n    $got,\n    { name => 'Alice', roles => ['admin'] },\n    'returns expected structure'\n);\n\n# Pattern matching\nlike($error, qr\u002Fnot found\u002Fi, 'error mentions not found');\nunlike($output, qr\u002Fpassword\u002F, 'output hides password');\n\n# Type check\nisa_ok($obj, 'MyApp::User');\ncan_ok($obj, 'save', 'delete');\n\ndone_testing;\n```\n\n### SKIP 和 TODO\n\n```perl\nuse v5.36;\nuse Test::More;\n\n# Skip tests conditionally\nSKIP: {\n    skip 'No database configured', 2 unless $ENV{TEST_DB};\n\n    my $db = connect_db();\n    ok($db->ping, 'database is reachable');\n    is($db->version, '15', 'correct PostgreSQL version');\n}\n\n# Mark expected failures\nTODO: {\n    local $TODO = 'Caching not yet implemented';\n    is($cache->get('key'), 'value', 'cache returns value');\n}\n\ndone_testing;\n```\n\n## Test2::V0 现代框架\n\nTest2::V0 是 Test::More 的现代替代品 —— 更丰富的断言、更好的诊断和可扩展性。\n\n### 为什么选择 Test2？\n\n* 使用哈希\u002F数组构建器进行卓越的深层比较\n* 失败时提供更好的诊断输出\n* 具有更清晰作用域的子测试\n* 可通过 Test2::Tools::\\* 插件扩展\n* 与 Test::More 测试向后兼容\n\n### 使用构建器进行深层比较\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\n# Hash builder — check partial structure\nis(\n    $user->to_hash,\n    hash {\n        field name  => 'Alice';\n        field email => match(qr\u002F\\@example\\.com$\u002F);\n        field age   => validator(sub { $_ >= 18 });\n        # Ignore other fields\n        etc();\n    },\n    'user has expected fields'\n);\n\n# Array builder\nis(\n    $result,\n    array {\n        item 'first';\n        item match(qr\u002F^second\u002F);\n        item DNE();  # Does Not Exist — verify no extra items\n    },\n    'result matches expected list'\n);\n\n# Bag — order-independent comparison\nis(\n    $tags,\n    bag {\n        item 'perl';\n        item 'testing';\n        item 'tdd';\n    },\n    'has all required tags regardless of order'\n);\n```\n\n### 子测试\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\nsubtest 'User creation' => sub {\n    my $user = User->new(name => 'Alice', email => 'alice@example.com');\n    ok($user, 'user object created');\n    is($user->name, 'Alice', 'name is set');\n    is($user->email, 'alice@example.com', 'email is set');\n};\n\nsubtest 'User validation' => sub {\n    my $warnings = warns {\n        User->new(name => '', email => 'bad');\n    };\n    ok($warnings, 'warns on invalid data');\n};\n\ndone_testing;\n```\n\n### 使用 Test2 进行异常测试\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\n# Test that code dies\nlike(\n    dies { divide(10, 0) },\n    qr\u002FDivision by zero\u002F,\n    'dies on division by zero'\n);\n\n# Test that code lives\nok(lives { divide(10, 2) }, 'division succeeds') or note($@);\n\n# Combined pattern\nsubtest 'error handling' => sub {\n    ok(lives { parse_config('valid.json') }, 'valid config parses');\n    like(\n        dies { parse_config('missing.json') },\n        qr\u002FCannot open\u002F,\n        'missing file dies with message'\n    );\n};\n\ndone_testing;\n```\n\n## 测试组织与 prove\n\n### 目录结构\n\n```text\nt\u002F\n├── 00-load.t              # 验证模块编译\n├── 01-basic.t             # 核心功能\n├── unit\u002F\n│   ├── config.t           # 按模块划分的单元测试\n│   ├── user.t\n│   └── util.t\n├── integration\u002F\n│   ├── database.t\n│   └── api.t\n├── lib\u002F\n│   └── TestHelper.pm      # 共享测试工具\n└── fixtures\u002F\n    ├── config.json        # 测试数据文件\n    └── users.csv\n```\n\n### prove 命令\n\n```bash\n# Run all tests\nprove -l t\u002F\n\n# Verbose output\nprove -lv t\u002F\n\n# Run specific test\nprove -lv t\u002Funit\u002Fuser.t\n\n# Recursive search\nprove -lr t\u002F\n\n# Parallel execution (8 jobs)\nprove -lr -j8 t\u002F\n\n# Run only failing tests from last run\nprove -l --state=failed t\u002F\n\n# Colored output with timer\nprove -l --color --timer t\u002F\n\n# TAP output for CI\nprove -l --formatter TAP::Formatter::JUnit t\u002F > results.xml\n```\n\n### .proverc 配置\n\n```text\n-l\n--color\n--timer\n-r\n-j4\n--state=save\n```\n\n## 夹具与设置\u002F拆卸\n\n### 子测试隔离\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse File::Temp qw(tempdir);\nuse Path::Tiny;\n\nsubtest 'file processing' => sub {\n    # Setup\n    my $dir = tempdir(CLEANUP => 1);\n    my $file = path($dir, 'input.txt');\n    $file->spew_utf8(\"line1\\nline2\\nline3\\n\");\n\n    # Test\n    my $result = process_file(\"$file\");\n    is($result->{line_count}, 3, 'counts lines');\n\n    # Teardown happens automatically (CLEANUP => 1)\n};\n```\n\n### 共享测试助手\n\n将可重用的助手放在 `t\u002Flib\u002FTestHelper.pm` 中，并通过 `use lib 't\u002Flib'` 加载。通过 `Exporter` 导出工厂函数，例如 `create_test_db()`、`create_temp_dir()` 和 `fixture_path()`。\n\n## 模拟\n\n### Test::MockModule\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse Test::MockModule;\n\nsubtest 'mock external API' => sub {\n    my $mock = Test::MockModule->new('MyApp::API');\n\n    # Good: Mock returns controlled data\n    $mock->mock(fetch_user => sub ($self, $id) {\n        return { id => $id, name => 'Mock User', email => 'mock@test.com' };\n    });\n\n    my $api = MyApp::API->new;\n    my $user = $api->fetch_user(42);\n    is($user->{name}, 'Mock User', 'returns mocked user');\n\n    # Verify call count\n    my $call_count = 0;\n    $mock->mock(fetch_user => sub { $call_count++; return {} });\n    $api->fetch_user(1);\n    $api->fetch_user(2);\n    is($call_count, 2, 'fetch_user called twice');\n\n    # Mock is automatically restored when $mock goes out of scope\n};\n\n# Bad: Monkey-patching without restoration\n# *MyApp::API::fetch_user = sub { ... };  # NEVER — leaks across tests\n```\n\n对于轻量级的模拟对象，使用 `Test::MockObject` 创建可注入的测试替身，使用 `->mock()` 并验证调用 `->called_ok()`。\n\n## 使用 Devel::Cover 进行覆盖率分析\n\n### 运行覆盖率分析\n\n```bash\n# Basic coverage report\ncover -test\n\n# Or step by step\nperl -MDevel::Cover -Ilib t\u002Funit\u002Fuser.t\ncover\n\n# HTML report\ncover -report html\nopen cover_db\u002Fcoverage.html\n\n# Specific thresholds\ncover -test -report text | grep 'Total'\n\n# CI-friendly: fail under threshold\ncover -test && cover -report text -select '^lib\u002F' \\\n  | perl -ne 'if (\u002FTotal.*?(\\d+\\.\\d+)\u002F) { exit 1 if $1 \u003C 80 }'\n```\n\n### 集成测试\n\n对数据库测试使用内存中的 SQLite，对 API 测试模拟 HTTP::Tiny。\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse DBI;\n\nsubtest 'database integration' => sub {\n    my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {\n        RaiseError => 1,\n    });\n    $dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');\n\n    $dbh->prepare('INSERT INTO users (name) VALUES (?)')->execute('Alice');\n    my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE name = ?', undef, 'Alice');\n    is($row->{name}, 'Alice', 'inserted and retrieved user');\n};\n\ndone_testing;\n```\n\n## 最佳实践\n\n### 应做事项\n\n* **遵循 TDD**：在实现之前编写测试（红-绿-重构）\n* **使用 Test2::V0**：现代断言，更好的诊断\n* **使用子测试**：分组相关断言，隔离状态\n* **模拟外部依赖**：网络、数据库、文件系统\n* **使用 `prove -l`**：始终将 lib\u002F 包含在 `@INC` 中\n* **清晰命名测试**：`'user login with invalid password fails'`\n* **测试边界情况**：空字符串、undef、零、边界值\n* **目标 80%+ 覆盖率**：专注于业务逻辑路径\n* **保持测试快速**：模拟 I\u002FO，使用内存数据库\n\n### 禁止事项\n\n* **不要测试实现**：测试行为和输出，而非内部细节\n* **不要在子测试之间共享状态**：每个子测试都应是独立的\n* **不要跳过 `done_testing`**：确保所有计划的测试都已运行\n* **不要过度模拟**：仅模拟边界，而非被测试的代码\n* **不要在新项目中使用 `Test::More`**：首选 Test2::V0\n* **不要忽略测试失败**：所有测试必须在合并前通过\n* **不要测试 CPAN 模块**：相信库能正常工作\n* **不要编写脆弱的测试**：避免过度具体的字符串匹配\n\n## 快速参考\n\n| 任务 | 命令 \u002F 模式 |\n|---|---|\n| 运行所有测试 | `prove -lr t\u002F` |\n| 详细运行单个测试 | `prove -lv t\u002Funit\u002Fuser.t` |\n| 并行测试运行 | `prove -lr -j8 t\u002F` |\n| 覆盖率报告 | `cover -test && cover -report html` |\n| 测试相等性 | `is($got, $expected, 'label')` |\n| 深层比较 | `is($got, hash { field k => 'v'; etc() }, 'label')` |\n| 测试异常 | `like(dies { ... }, qr\u002Fmsg\u002F, 'label')` |\n| 测试无异常 | `ok(lives { ... }, 'label')` |\n| 模拟一个方法 | `Test::MockModule->new('Pkg')->mock(m => sub { ... })` |\n| 跳过测试 | `SKIP: { skip 'reason', $count unless $cond; ... }` |\n| TODO 测试 | `TODO: { local $TODO = 'reason'; ... }` |\n\n## 常见陷阱\n\n### 忘记 `done_testing`\n\n```perl\n# Bad: Test file runs but doesn't verify all tests executed\nuse Test2::V0;\nis(1, 1, 'works');\n# Missing done_testing — silent bugs if test code is skipped\n\n# Good: Always end with done_testing\nuse Test2::V0;\nis(1, 1, 'works');\ndone_testing;\n```\n\n### 缺少 `-l` 标志\n\n```bash\n# Bad: Modules in lib\u002F not found\nprove t\u002Funit\u002Fuser.t\n# Can't locate MyApp\u002FUser.pm in @INC\n\n# Good: Include lib\u002F in @INC\nprove -l t\u002Funit\u002Fuser.t\n```\n\n### 过度模拟\n\n模拟*依赖项*，而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容，那么它什么也没测试。\n\n### 测试污染\n\n在子测试内部使用 `my` 变量 —— 永远不要用 `our` —— 以防止状态在测试之间泄漏。\n\n**记住**：测试是你的安全网。保持它们快速、专注和独立。新项目使用 Test2::V0，运行使用 prove，问责使用 Devel::Cover。\n","# Perl 测试模式\n\n使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。\n\n## 何时激活\n\n* 编写新的 Perl 代码（遵循 TDD：红、绿、重构）\n* 为 Perl 模块或应用程序设计测试套件\n* 审查 Perl 测试覆盖率\n* 设置 Perl 测试基础设施\n* 将测试从 Test::More 迁移到 Test2::V0\n* 调试失败的 Perl 测试\n\n## TDD 工作流程\n\n始终遵循 RED-GREEN-REFACTOR 循环。\n\n```perl\n# Step 1: RED — Write a failing test\n# t\u002Funit\u002Fcalculator.t\nuse v5.36;\nuse Test2::V0;\n\nuse lib 'lib';\nuse Calculator;\n\nsubtest 'addition' => sub {\n    my $calc = Calculator->new;\n    is($calc->add(2, 3), 5, 'adds two numbers');\n    is($calc->add(-1, 1), 0, 'handles negatives');\n};\n\ndone_testing;\n\n# Step 2: GREEN — Write minimal implementation\n# lib\u002FCalculator.pm\npackage Calculator;\nuse v5.36;\nuse Moo;\n\nsub add($self, $a, $b) {\n    return $a + $b;\n}\n\n1;\n\n# Step 3: REFACTOR — Improve while tests stay green\n# Run: prove -lv t\u002Funit\u002Fcalculator.t\n```\n\n## Test::More 基础\n\n标准的 Perl 测试模块 —— 广泛使用，随核心发行。\n\n### 基本断言\n\n```perl\nuse v5.36;\nuse Test::More;\n\n# Plan upfront or use done_testing\n# plan tests => 5;  # Fixed plan (optional)\n\n# Equality\nis($result, 42, 'returns correct value');\nisnt($result, 0, 'not zero');\n\n# Boolean\nok($user->is_active, 'user is active');\nok(!$user->is_banned, 'user is not banned');\n\n# Deep comparison\nis_deeply(\n    $got,\n    { name => 'Alice', roles => ['admin'] },\n    'returns expected structure'\n);\n\n# Pattern matching\nlike($error, qr\u002Fnot found\u002Fi, 'error mentions not found');\nunlike($output, qr\u002Fpassword\u002F, 'output hides password');\n\n# Type check\nisa_ok($obj, 'MyApp::User');\ncan_ok($obj, 'save', 'delete');\n\ndone_testing;\n```\n\n### SKIP 和 TODO\n\n```perl\nuse v5.36;\nuse Test::More;\n\n# Skip tests conditionally\nSKIP: {\n    skip 'No database configured', 2 unless $ENV{TEST_DB};\n\n    my $db = connect_db();\n    ok($db->ping, 'database is reachable');\n    is($db->version, '15', 'correct PostgreSQL version');\n}\n\n# Mark expected failures\nTODO: {\n    local $TODO = 'Caching not yet implemented';\n    is($cache->get('key'), 'value', 'cache returns value');\n}\n\ndone_testing;\n```\n\n## Test2::V0 现代框架\n\nTest2::V0 是 Test::More 的现代替代品 —— 更丰富的断言、更好的诊断和可扩展性。\n\n### 为什么选择 Test2？\n\n* 使用哈希\u002F数组构建器进行卓越的深层比较\n* 失败时提供更好的诊断输出\n* 具有更清晰作用域的子测试\n* 可通过 Test2::Tools::\\* 插件扩展\n* 与 Test::More 测试向后兼容\n\n### 使用构建器进行深层比较\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\n# Hash builder — check partial structure\nis(\n    $user->to_hash,\n    hash {\n        field name  => 'Alice';\n        field email => match(qr\u002F\\@example\\.com$\u002F);\n        field age   => validator(sub { $_ >= 18 });\n        # Ignore other fields\n        etc();\n    },\n    'user has expected fields'\n);\n\n# Array builder\nis(\n    $result,\n    array {\n        item 'first';\n        item match(qr\u002F^second\u002F);\n        item DNE();  # Does Not Exist — verify no extra items\n    },\n    'result matches expected list'\n);\n\n# Bag — order-independent comparison\nis(\n    $tags,\n    bag {\n        item 'perl';\n        item 'testing';\n        item 'tdd';\n    },\n    'has all required tags regardless of order'\n);\n```\n\n### 子测试\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\nsubtest 'User creation' => sub {\n    my $user = User->new(name => 'Alice', email => 'alice@example.com');\n    ok($user, 'user object created');\n    is($user->name, 'Alice', 'name is set');\n    is($user->email, 'alice@example.com', 'email is set');\n};\n\nsubtest 'User validation' => sub {\n    my $warnings = warns {\n        User->new(name => '', email => 'bad');\n    };\n    ok($warnings, 'warns on invalid data');\n};\n\ndone_testing;\n```\n\n### 使用 Test2 进行异常测试\n\n```perl\nuse v5.36;\nuse Test2::V0;\n\n# Test that code dies\nlike(\n    dies { divide(10, 0) },\n    qr\u002FDivision by zero\u002F,\n    'dies on division by zero'\n);\n\n# Test that code lives\nok(lives { divide(10, 2) }, 'division succeeds') or note($@);\n\n# Combined pattern\nsubtest 'error handling' => sub {\n    ok(lives { parse_config('valid.json') }, 'valid config parses');\n    like(\n        dies { parse_config('missing.json') },\n        qr\u002FCannot open\u002F,\n        'missing file dies with message'\n    );\n};\n\ndone_testing;\n```\n\n## 测试组织与 prove\n\n### 目录结构\n\n```text\nt\u002F\n├── 00-load.t              # 验证模块编译\n├── 01-basic.t             # 核心功能\n├── unit\u002F\n│   ├── config.t           # 按模块划分的单元测试\n│   ├── user.t\n│   └── util.t\n├── integration\u002F\n│   ├── database.t\n│   └── api.t\n├── lib\u002F\n│   └── TestHelper.pm      # 共享测试工具\n└── fixtures\u002F\n    ├── config.json        # 测试数据文件\n    └── users.csv\n```\n\n### prove 命令\n\n```bash\n# Run all tests\nprove -l t\u002F\n\n# Verbose output\nprove -lv t\u002F\n\n# Run specific test\nprove -lv t\u002Funit\u002Fuser.t\n\n# Recursive search\nprove -lr t\u002F\n\n# Parallel execution (8 jobs)\nprove -lr -j8 t\u002F\n\n# Run only failing tests from last run\nprove -l --state=failed t\u002F\n\n# Colored output with timer\nprove -l --color --timer t\u002F\n\n# TAP output for CI\nprove -l --formatter TAP::Formatter::JUnit t\u002F > results.xml\n```\n\n### .proverc 配置\n\n```text\n-l\n--color\n--timer\n-r\n-j4\n--state=save\n```\n\n## 夹具与设置\u002F拆卸\n\n### 子测试隔离\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse File::Temp qw(tempdir);\nuse Path::Tiny;\n\nsubtest 'file processing' => sub {\n    # Setup\n    my $dir = tempdir(CLEANUP => 1);\n    my $file = path($dir, 'input.txt');\n    $file->spew_utf8(\"line1\\nline2\\nline3\\n\");\n\n    # Test\n    my $result = process_file(\"$file\");\n    is($result->{line_count}, 3, 'counts lines');\n\n    # Teardown happens automatically (CLEANUP => 1)\n};\n```\n\n### 共享测试助手\n\n将可重用的助手放在 `t\u002Flib\u002FTestHelper.pm` 中，并通过 `use lib 't\u002Flib'` 加载。通过 `Exporter` 导出工厂函数，例如 `create_test_db()`、`create_temp_dir()` 和 `fixture_path()`。\n\n## 模拟\n\n### Test::MockModule\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse Test::MockModule;\n\nsubtest 'mock external API' => sub {\n    my $mock = Test::MockModule->new('MyApp::API');\n\n    # Good: Mock returns controlled data\n    $mock->mock(fetch_user => sub ($self, $id) {\n        return { id => $id, name => 'Mock User', email => 'mock@test.com' };\n    });\n\n    my $api = MyApp::API->new;\n    my $user = $api->fetch_user(42);\n    is($user->{name}, 'Mock User', 'returns mocked user');\n\n    # Verify call count\n    my $call_count = 0;\n    $mock->mock(fetch_user => sub { $call_count++; return {} });\n    $api->fetch_user(1);\n    $api->fetch_user(2);\n    is($call_count, 2, 'fetch_user called twice');\n\n    # Mock is automatically restored when $mock goes out of scope\n};\n\n# Bad: Monkey-patching without restoration\n# *MyApp::API::fetch_user = sub { ... };  # NEVER — leaks across tests\n```\n\n对于轻量级的模拟对象，使用 `Test::MockObject` 创建可注入的测试替身，使用 `->mock()` 并验证调用 `->called_ok()`。\n\n## 使用 Devel::Cover 进行覆盖率分析\n\n### 运行覆盖率分析\n\n```bash\n# Basic coverage report\ncover -test\n\n# Or step by step\nperl -MDevel::Cover -Ilib t\u002Funit\u002Fuser.t\ncover\n\n# HTML report\ncover -report html\nopen cover_db\u002Fcoverage.html\n\n# Specific thresholds\ncover -test -report text | grep 'Total'\n\n# CI-friendly: fail under threshold\ncover -test && cover -report text -select '^lib\u002F' \\\n  | perl -ne 'if (\u002FTotal.*?(\\d+\\.\\d+)\u002F) { exit 1 if $1 \u003C 80 }'\n```\n\n### 集成测试\n\n对数据库测试使用内存中的 SQLite，对 API 测试模拟 HTTP::Tiny。\n\n```perl\nuse v5.36;\nuse Test2::V0;\nuse DBI;\n\nsubtest 'database integration' => sub {\n    my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {\n        RaiseError => 1,\n    });\n    $dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');\n\n    $dbh->prepare('INSERT INTO users (name) VALUES (?)')->execute('Alice');\n    my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE name = ?', undef, 'Alice');\n    is($row->{name}, 'Alice', 'inserted and retrieved user');\n};\n\ndone_testing;\n```\n\n## 最佳实践\n\n### 应做事项\n\n* **遵循 TDD**：在实现之前编写测试（红-绿-重构）\n* **使用 Test2::V0**：现代断言，更好的诊断\n* **使用子测试**：分组相关断言，隔离状态\n* **模拟外部依赖**：网络、数据库、文件系统\n* **使用 `prove -l`**：始终将 lib\u002F 包含在 `@INC` 中\n* **清晰命名测试**：`'user login with invalid password fails'`\n* **测试边界情况**：空字符串、undef、零、边界值\n* **目标 80%+ 覆盖率**：专注于业务逻辑路径\n* **保持测试快速**：模拟 I\u002FO，使用内存数据库\n\n### 禁止事项\n\n* **不要测试实现**：测试行为和输出，而非内部细节\n* **不要在子测试之间共享状态**：每个子测试都应是独立的\n* **不要跳过 `done_testing`**：确保所有计划的测试都已运行\n* **不要过度模拟**：仅模拟边界，而非被测试的代码\n* **不要在新项目中使用 `Test::More`**：首选 Test2::V0\n* **不要忽略测试失败**：所有测试必须在合并前通过\n* **不要测试 CPAN 模块**：相信库能正常工作\n* **不要编写脆弱的测试**：避免过度具体的字符串匹配\n\n## 快速参考\n\n| 任务 | 命令 \u002F 模式 |\n|---|---|\n| 运行所有测试 | `prove -lr t\u002F` |\n| 详细运行单个测试 | `prove -lv t\u002Funit\u002Fuser.t` |\n| 并行测试运行 | `prove -lr -j8 t\u002F` |\n| 覆盖率报告 | `cover -test && cover -report html` |\n| 测试相等性 | `is($got, $expected, 'label')` |\n| 深层比较 | `is($got, hash { field k => 'v'; etc() }, 'label')` |\n| 测试异常 | `like(dies { ... }, qr\u002Fmsg\u002F, 'label')` |\n| 测试无异常 | `ok(lives { ... }, 'label')` |\n| 模拟一个方法 | `Test::MockModule->new('Pkg')->mock(m => sub { ... })` |\n| 跳过测试 | `SKIP: { skip 'reason', $count unless $cond; ... }` |\n| TODO 测试 | `TODO: { local $TODO = 'reason'; ... }` |\n\n## 常见陷阱\n\n### 忘记 `done_testing`\n\n```perl\n# Bad: Test file runs but doesn't verify all tests executed\nuse Test2::V0;\nis(1, 1, 'works');\n# Missing done_testing — silent bugs if test code is skipped\n\n# Good: Always end with done_testing\nuse Test2::V0;\nis(1, 1, 'works');\ndone_testing;\n```\n\n### 缺少 `-l` 标志\n\n```bash\n# Bad: Modules in lib\u002F not found\nprove t\u002Funit\u002Fuser.t\n# Can't locate MyApp\u002FUser.pm in @INC\n\n# Good: Include lib\u002F in @INC\nprove -l t\u002Funit\u002Fuser.t\n```\n\n### 过度模拟\n\n模拟*依赖项*，而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容，那么它什么也没测试。\n\n### 测试污染\n\n在子测试内部使用 `my` 变量 —— 永远不要用 `our` —— 以防止状态在测试之间泄漏。\n\n**记住**：测试是你的安全网。保持它们快速、专注和独立。新项目使用 Test2::V0，运行使用 prove，问责使用 Devel::Cover。\n",{"name":5,"origin":33,"description":7},"ECC","main","a2e3284ed4488780397ee2f78217366e11470dfb","skills\u002Fperl-testing","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright","https:\u002F\u002Fplaywright.dev","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack","2026-04-25 06:04:45",{"source":8,"skillName":5,"commands":42,"prompt":52,"zipAvailable":53},[43,46,49],{"tool":44,"command":45},"npx","npx skills add 7dvq6yrkgs-stack\u002Fplaywright --skill perl-testing",{"tool":47,"command":48},"bunx","bunx skills add 7dvq6yrkgs-stack\u002Fplaywright --skill perl-testing",{"tool":50,"command":51},"pnpm","pnpm dlx skills add 7dvq6yrkgs-stack\u002Fplaywright --skill perl-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fperl-testing\u002FSKILL.md",true,[55],{"path":56,"resourceType":57,"resourceTypeDesc":58,"sizeBytes":59,"sourceHtmlUrl":60},".skillfish.json","REFERENCE","参考文档",242,"https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fperl-testing\u002F.skillfish.json",[62,92,102,112,127,142],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":80,"publishedAt":28},"01M6E3XK4PRPMQHM7Y98FQPTY1","zyte-api-automation","Automate Zyte API tasks via Rube MCP (Composio). Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzyte-api-automation\u002FSKILL.md",[68,69,72,75],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":70,"categoryName":71,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[81,82,83,84,85,86,87,88,89,90,91],"zyte api automation","api","automation","MCP","tasks","search","Tools","first","automate","composio","via",{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":100,"publishedAt":28},"01M6E3XFX6QSTR1QR59NFXJTMA","zylvie-automation","Automate Zylvie tasks via Rube MCP (Composio). Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzylvie-automation\u002FSKILL.md",[98,99],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},[101,83,89,84,85,86,87,88,90,91],"zylvie automation",{"skillNo":103,"name":104,"nameZh":6,"description":105,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":110,"publishedAt":28},"01M6E3XCX2GDYYCHN58ZJHT37M","zoominfo-automation","Automate Zoominfo tasks via Rube MCP (Composio). Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzoominfo-automation\u002FSKILL.md",[108,109],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},[111,83,86,87,88,89,90,91,85,84],"zoominfo automation",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":120,"publishedAt":28},"01M6E3X9WJ2T73216WQHHTB9BD","zoho_mail-automation","Automate Zoho Mail tasks via Rube MCP (Composio): email sending, folders, labels, and mailbox management. Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzoho_mail-automation\u002FSKILL.md",[118,119],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},[121,122,123,83,84,85,86,124,87,88,89,125,90,126,91],"mail","zoho mail automation","zoho","email","management","mailbox",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":138,"publishedAt":28},"01M6E3X719W648XZBW1EC6JQ64","zoho_invoice-automation","Automate Zoho Invoice tasks via Rube MCP (Composio): invoices, estimates, expenses, clients, and payment tracking. Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzoho_invoice-automation\u002FSKILL.md",[133,134,135],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":136,"categoryName":137,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000040","财务",[139,123,83,85,86,87,88,89,140,141,90,91,84],"zoho invoice automation","tracking","payment",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":150,"publishedAt":28},"01M6E3X28JYTC3QTK32AQF7BYB","zoho_inventory-automation","Automate Zoho Inventory tasks via Rube MCP (Composio): items, orders, warehouses, shipments, and stock management. Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002F7dvq6yrkgs-stack\u002Fplaywright\u002Fblob\u002Fmain\u002Fskills\u002Fzoho_inventory-automation\u002FSKILL.md",[148,149],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":73,"categoryName":74,"taxonomyType":20,"taxonomyTypeDesc":21},[83,151,123,152,88,89,87,86,85,84,90,91,153,154,125],"inventory","zoho inventory automation","stock","orders"]