perl-testing
affaan-m/ECC
Test2::V0, Test::More, prove 실행기, 모킹 및 Devel::Cover를 활용한 커버리지 분석을 통해 Perl 애플리케이션을 위한 포괄적인 테스트 패턴을 제공합니다.
...모든 것을 확장하십시오Perl 테스트 패턴
Test2::V0, Test::More, prove 및 TDD 방법론을 활용한 Perl 애플리케이션을 위한 포괄적인 테스트 전략.
사용 시점
- 새로운 Perl 코드를 작성할 때 (TDD 준수: 레드, 그린, 리팩토링)
- Perl 모듈 또는 애플리케이션을 위한 테스트 스위트 설계 시
- Perl 테스트 커버리지 검토
- Perl 테스트 인프라 구축
- Test::More에서 Test2::V0로 테스트 마이그레이션
- 실패한 Perl 테스트 디버깅
TDD 워크플로우
항상 RED-GREEN-REFACTOR 주기를 따르세요.
# Step 1: RED — Write a failing test
# t/unit/calculator.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;
# Step 2: GREEN — Write minimal implementation
# lib/Calculator.pm
package 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 테스트 모듈 — 널리 사용되며, 코어에 포함되어 있습니다.
기본 어설션
use v5.36;
use Test::More;
# Plan upfront or use done_testing
# plan tests => 5; # Fixed plan (optional)
# Equality
is($result, 42, 'returns correct value');
isnt($result, 0, 'not zero');
# Boolean
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
# Deep comparison
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
# Pattern matching
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
# Type check
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
SKIP 및 TODO
use v5.36;
use Test::More;
# 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');
}
# Mark expected failures
TODO: {
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 테스트와의 하위 호환성
빌더를 활용한 심층 비교
use v5.36;
use Test2::V0;
# 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'
);
# Array builder
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
is(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'has all required tags regardless of order'
);
하위 테스트
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를 이용한 예외 테스트
use v5.36;
use Test2::V0;
# Test that code dies
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
# Test that code lives
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
# 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;
테스트 구성 및 prove
디렉터리 구조
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 명령어
# Run all tests
prove -l t/
# Verbose output
prove -lv t/
# Run specific test
prove -lv t/unit/user.t
# Recursive search
prove -lr t/
# Parallel execution (8 jobs)
prove -lr -j8 t/
# Run only failing tests from last run
prove -l --state=failed t/
# Colored output with timer
prove -l --color --timer t/
# TAP output for CI
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
.proverc 구성
-l
--color
--timer
-r
-j4
--state=save
피처 및 설정/종료
하위 테스트 격리
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'로 불러옵니다. 다음과 같은 팩토리 함수는 create_test_db(), create_temp_dir()와 같은 팩토리 함수를 내보내고 fixture_path() 를 통해 Exporter.
모킹
Test::MockModule
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를 사용한 커버리지
커버리지 실행
# Basic coverage report
cover -test
# Or step by step
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# HTML report
cover -report html
open cover_db/coverage.html
# Specific thresholds
cover -test -report text | grep 'Total'
# CI-friendly: fail under threshold
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
통합 테스트
데이터베이스 테스트에는 인메모리 SQLite를, API 테스트에는 HTTP::Tiny 모의 객체를 사용하십시오.
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 사용: 최신 어설션, 향상된 진단 기능
- 서브테스트를 사용하세요: 관련 어설션을 그룹화하고 상태를 분리하세요
- 외부 종속성을 모의 객체(Mock)로 구현하세요: 네트워크, 데이터베이스, 파일 시스템
prove -l사용: 항상 lib/를 포함시키세요@INC- 테스트 이름을 명확하게 지정하세요:
'user login with invalid password fails' - 경계 사례 테스트: 빈 문자열, undef, 0, 경계값
- 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
# Bad: Test file runs but doesn't verify all tests executed
use Test2::V0;
is(1, 1, 'works');
# Missing done_testing — silent bugs if test code is skipped
# Good: Always end with done_testing
use Test2::V0;
is(1, 1, 'works');
done_testing;
누락 -l 표시
# Bad: Modules in lib/ not found
prove t/unit/user.t
# Can't locate MyApp/User.pm in @INC
# Good: Include lib/ in @INC
prove -l t/unit/user.t
과도한 모킹
테스트 대상 코드가 아닌 의존성을 모킹하십시오. 테스트가 모킹된 객체가 지시한 대로 값을 반환하는지 확인하는 데 그친다면, 실제로는 아무것도 테스트하지 않는 것입니다.
테스트 오염
서브테스트 내에서는 my 변수를 사용하십시오 — 절대로 our — 테스트 간 상태 유출을 방지하기 위해 사용하지 마십시오.
기억하세요: 테스트는 여러분의 안전망입니다. 테스트를 빠르고, 집중적이며, 독립적으로 유지하십시오. 새 프로젝트에는 Test2::V0를, 실행 시에는 prove를, 책임성 확보를 위해 Devel::Cover를 사용하십시오.
---
name: perl-testing
description: Provides comprehensive testing patterns for Perl applications using Test2::V0, Test::More, prove runner, mocking, and coverage analysis with Devel::Cover.
---
# Perl Testing Patterns
Comprehensive testing strategies for Perl applications using Test2::V0, Test::More, prove, and TDD methodology.
## 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
## TDD Workflow
Always follow the RED-GREEN-REFACTOR cycle.
```perl
# Step 1: RED — Write a failing test
# t/unit/calculator.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;
# Step 2: GREEN — Write minimal implementation
# lib/Calculator.pm
package 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 Fundamentals
The standard Perl testing module — widely used, ships with core.
### Basic Assertions
```perl
use v5.36;
use Test::More;
# Plan upfront or use done_testing
# plan tests => 5; # Fixed plan (optional)
# Equality
is($result, 42, 'returns correct value');
isnt($result, 0, 'not zero');
# Boolean
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
# Deep comparison
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
# Pattern matching
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
# Type check
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
```
### SKIP and TODO
```perl
use v5.36;
use Test::More;
# 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');
}
# Mark expected failures
TODO: {
local $TODO = 'Caching not yet implemented';
is($cache->get('key'), 'value', 'cache returns value');
}
done_testing;
```
## Test2::V0 Modern Framework
Test2::V0 is the modern replacement for Test::More — richer assertions, better diagnostics, and extensible.
### Why Test2?
- 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
### Deep Comparison with Builders
```perl
use v5.36;
use Test2::V0;
# 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'
);
# Array builder
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
is(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'has all required tags regardless of order'
);
```
### Subtests
```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;
```
### Exception Testing with Test2
```perl
use v5.36;
use Test2::V0;
# Test that code dies
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
# Test that code lives
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
# 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;
```
## Test Organization and 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
```
### prove Commands
```bash
# Run all tests
prove -l t/
# Verbose output
prove -lv t/
# Run specific test
prove -lv t/unit/user.t
# Recursive search
prove -lr t/
# Parallel execution (8 jobs)
prove -lr -j8 t/
# Run only failing tests from last run
prove -l --state=failed t/
# Colored output with timer
prove -l --color --timer t/
# TAP output for CI
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
```
### .proverc Configuration
```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)
};
```
### 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`.
## Mocking
### 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
```
For lightweight mock objects, use `Test::MockObject` to create injectable test doubles with `->mock()` and verify calls with `->called_ok()`.
## Coverage with Devel::Cover
### Running Coverage
```bash
# Basic coverage report
cover -test
# Or step by step
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# HTML report
cover -report html
open cover_db/coverage.html
# Specific thresholds
cover -test -report text | grep 'Total'
# CI-friendly: fail under threshold
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
```
### 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;
```
## 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
### 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
## Quick Reference
| Task | Command / 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'; ... }` |
## Common Pitfalls
### Forgetting `done_testing`
```perl
# Bad: Test file runs but doesn't verify all tests executed
use Test2::V0;
is(1, 1, 'works');
# Missing done_testing — silent bugs if test code is skipped
# Good: Always end with done_testing
use Test2::V0;
is(1, 1, 'works');
done_testing;
```
### Missing `-l` Flag
```bash
# Bad: Modules in lib/ not found
prove t/unit/user.t
# Can't locate MyApp/User.pm in @INC
# Good: Include lib/ in @INC
prove -l t/unit/user.t
```
### Over-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.
### 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.
모든 파일
1개 파일perl-testing 설치
스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/affaan-m/ECC/tree/main/skills/perl-testing # Copy SKILL.md to your .claude/skills/ directory
복사





집
