From ecc
Test2::V0, Test::More, prove 실행기, 모킹, Devel::Cover를 이용한 커버리지 및 TDD 방법론을 사용하는 Perl 테스트 패턴입니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
Test2::V0, Test::More, prove 및 TDD 방법론을 사용한 Perl 애플리케이션을 위한 포괄적인 테스트 전략입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Test2::V0, Test::More, prove 및 TDD 방법론을 사용한 Perl 애플리케이션을 위한 포괄적인 테스트 전략입니다.
항상 RED-GREEN-REFACTOR 사이클을 따르십시오.
# 1단계: RED — 실패하는 테스트 작성
# 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;
# 2단계: GREEN — 최소한의 구현 작성
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# 3단계: REFACTOR — 테스트가 green 상태를 유지하는 동안 개선
# 실행: prove -lv t/unit/calculator.t
표준 Perl 테스트 모듈 — 널리 사용되며 코어에 포함되어 있습니다.
use v5.36;
use Test::More;
# 계획을 미리 세우거나 done_testing 사용
# plan tests => 5; # 고정된 계획 (선택 사항)
# 일치 여부
is($result, 42, 'returns correct value');
isnt($result, 0, 'not zero');
# 불리언
ok($user->is_active, 'user is active');
ok(!$user->is_banned, 'user is not banned');
# 깊은 비교
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'returns expected structure'
);
# 패턴 매칭
like($error, qr/not found/i, 'error mentions not found');
unlike($output, qr/password/, 'output hides password');
# 타입 확인
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
use v5.36;
use Test::More;
# 조건부로 테스트 건너뛰기
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');
}
# 예상되는 실패 표시
TODO: {
local $TODO = 'Caching not yet implemented';
is($cache->get('key'), 'value', 'cache returns value');
}
done_testing;
Test2::V0는 Test::More의 현대적 대체품입니다 — 더 풍부한 단언문, 더 나은 진단 기능 및 확장성을 제공합니다.
use v5.36;
use Test2::V0;
# Hash 빌더 — 부분적인 구조 확인
is(
$user->to_hash,
hash {
field name => 'Alice';
field email => match(qr/\@example\.com$/);
field age => validator(sub { $_ >= 18 });
# 다른 필드는 무시
etc();
},
'user has expected fields'
);
# Array 빌더
is(
$result,
array {
item 'first';
item match(qr/^second/);
item DNE(); # Does Not Exist — 여분의 항목이 없는지 확인
},
'result matches expected list'
);
# Bag — 순서에 상관없는 비교
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 => '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;
use v5.36;
use Test2::V0;
# 코드가 die하는지 테스트
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
# 코드가 정상 실행(live)되는지 테스트
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
# 혼합 패턴
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;
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 -l t/
# 상세 출력
prove -lv t/
# 특정 테스트 실행
prove -lv t/unit/user.t
# 재귀적 탐색
prove -lr t/
# 병렬 실행 (8개 작업)
prove -lr -j8 t/
# 지난번 실행에서 실패한 테스트만 실행
prove -l --state=failed t/
# 타이머와 함께 컬러 출력
prove -l --color --timer t/
# CI를 위한 TAP 출력
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
-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은 자동으로 발생 (CLEANUP => 1)
};
재사용 가능한 헬퍼를 t/lib/TestHelper.pm에 두고 use lib 't/lib'로 로드하십시오. create_test_db(), create_temp_dir(), fixture_path()와 같은 팩토리 함수를 Exporter를 통해 내보냅니다.
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'mock external API' => sub {
my $mock = Test::MockModule->new('MyApp::API');
# 좋음: Mock이 제어된 데이터를 반환함
$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');
# 호출 횟수 확인
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이 스코프를 벗어나면 Mock은 자동으로 복구됨
};
# 나쁨: 복구 없이 몽키 패치(monkey-patching) 수행
# *MyApp::API::fetch_user = sub { ... }; # 절대 금지 — 테스트 간에 유출됨
가벼운 mock 객체를 위해서는 Test::MockObject를 사용하여 ->mock()으로 주입 가능한 테스트 더블을 생성하고 ->called_ok()로 호출을 확인하십시오.
# 기본 커버리지 보고서
cover -test
# 또는 단계별 실행
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# HTML 보고서
cover -report html
open cover_db/coverage.html
# 특정 임계값 확인
cover -test -report text | grep 'Total'
# CI 친화적: 임계값 미만 시 실패
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;
prove -l 사용: @INC에 항상 lib/를 포함하십시오'잘못된 비밀번호로 사용자 로그인 시 실패'done_testing을 생략하지 마십시오: 계획된 모든 테스트가 실행되었음을 보장합니다Test::More를 사용하지 마십시오: Test2::V0를 선호하십시오| 작업 | 명령어 / 패턴 |
|---|---|
| 모든 테스트 실행 | 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 망각# 나쁨: 테스트 파일이 실행되지만 모든 테스트가 수행되었는지 확인하지 않음
use Test2::V0;
is(1, 1, 'works');
# done_testing 누락 — 테스트 코드가 건너뛰어져도 조용히 넘어감
# 좋음: 항상 done_testing으로 끝냄
use Test2::V0;
is(1, 1, 'works');
done_testing;
-l 플래그 누락# 나쁨: lib/에 있는 모듈을 찾지 못함
prove t/unit/user.t
# Can't locate MyApp/User.pm in @INC
# 좋음: @INC에 lib/ 포함
prove -l t/unit/user.t
테스트 중인 코드가 아니라 의존성을 모킹하십시오. 테스트가 단지 모킹한 대로 반환되는지만 확인한다면 아무것도 테스트하지 않는 것입니다.
테스트 간에 상태가 유출되는 것을 방지하기 위해 subtests 내부에서는 our가 아닌 my 변수를 사용하십시오.
기억하십시오: 테스트는 여러분의 안전망입니다. 빠르고 집중적이며 독립적으로 유지하십시오. 새 프로젝트에는 Test2::V0를, 실행에는 prove를, 책임성 확인에는 Devel::Cover를 사용하십시오.