[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7AM5XRF35KVTAD58QP2YVH5":3,"skill-same:01M7AM5XRF35KVTAD58QP2YVH5":48},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":6,"ownerHtmlUrl":30,"ownerBlogUrl":31,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M7AM5XRF35KVTAD58QP2YVH5","perl-testing",null,"使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。","Cyrilthetecchieee\u002Fcyril-code-","Cyrilthetecchieee","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F219787998?v=4","01M7AF0MMMKRAA0H61XMJPV92H","Julian Cyril D ","01M7AF0MQ10AVG6QJQ1C2BC0F5",2,"MIT","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fperl-testing\u002FSKILL.md",[],[],"2026-11-15 14:19:17",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":24,"description":7},"ECC","main","57e7aa6b6e03bce1a7d7862ba9c2ec312f7f19b5","src\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fperl-testing",0,"https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee","https:\u002F\u002Fwithcyril.me\u002F","Hi, I’m Julian Cyril D, a Computer Science and Business Systems student at PSNACET.\r\nI’m passionate about full-stack development, UI\u002FUX design, and building sma","2026-11-04 23:28:50",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add Cyrilthetecchieee\u002Fcyril-code- --skill perl-testing",{"tool":40,"command":41},"bunx","bunx skills add Cyrilthetecchieee\u002Fcyril-code- --skill perl-testing",{"tool":43,"command":44},"pnpm","pnpm dlx skills add Cyrilthetecchieee\u002Fcyril-code- --skill perl-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fperl-testing\u002FSKILL.md",true,[],[49,58,65,73,80,87],{"skillNo":50,"name":51,"nameZh":6,"description":52,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":54,"sourceHtmlUrl":55,"categories":56,"keywords":57,"publishedAt":19},"01M7AQV6CYX8RMC78420V9WZ62","webapp-testing","Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fwebapp-testing\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fwebapp-testing\u002FSKILL.md",[],[],{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":64,"publishedAt":19},"01M7AQV2895N3S1MEX6K76G6G2","youtube-downloader","Download YouTube videos with customizable quality and format options. Use this skill when the user asks to download, save, or grab YouTube videos. Supports various quality settings (best, 1080p, 720p, 480p, 360p), multiple formats (mp4, webm, mkv), and audio-only downloads as MP3.","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fvideo-downloader\u002FSKILL.md",[],[],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":69,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":19},"01M7AQTRNX62DNKVZ5GXEHRBKB","theme-factory","Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors\u002Ffonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Ftheme-factory\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Ftheme-factory\u002FSKILL.md",[],[],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":19},"01M7AQTNNVHAAN9H6DA8T6ZVE0","template-skill","Replace with description of the skill and when Claude should use it.","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Ftemplate-skill\u002FSKILL.md",[],[],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":19},"01M7AQTJ3MQ6AKX152MEX2MYCC","tailored-resume-generator","Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Ftailored-resume-generator\u002FSKILL.md",[],[],{"skillNo":88,"name":89,"nameZh":6,"description":90,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":91,"sourceHtmlUrl":92,"categories":93,"keywords":94,"publishedAt":19},"01M7AQTD7D5MYET2YWG7HG9WM3","slack-gif-creator","Toolkit for creating animated GIFs optimized for Slack, with validators for size constraints and composable animation primitives. This skill applies when users request animated GIFs or emoji animations for Slack from descriptions like \"make me a GIF for Slack of X doing Y\".","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fslack-gif-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FCyrilthetecchieee\u002Fcyril-code-\u002Fblob\u002Fmain\u002Fsrc\u002Fcyril_code\u002Fassets\u002F.skills_repo\u002Fslack-gif-creator\u002FSKILL.md",[],[]]