| name | perl-testing |
| description | Test2::V0, Test::More, prove 실행기, 모킹, Devel::Cover를 이용한 커버리지 및 TDD 방법론을 사용하는 Perl 테스트 패턴입니다. |
| origin | ECC |
Perl 테스트 패턴
Test2::V0, Test::More, prove 및 TDD 방법론을 사용한 Perl 애플리케이션을 위한 포괄적인 테스트 전략입니다.
활성화 시점
- 새로운 Perl 코드를 작성할 때 (TDD 준수: red, green, refactor)
- Perl 모듈 또는 애플리케이션을 위한 테스트 스위트를 설계할 때
- Perl 테스트 커버리지를 검토할 때
- Perl 테스트 인프라를 구축할 때
- 테스트를 Test::More에서 Test2::V0로 마이그레이션할 때
- 실패하는 Perl 테스트를 디버깅할 때
TDD 워크플로
항상 RED-GREEN-REFACTOR 사이클을 따르십시오.
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;
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
Test::More 기초
표준 Perl 테스트 모듈 — 널리 사용되며 코어에 포함되어 있습니다.
기본 단언문 (Assertions)
use v5.36;
use Test::More;
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;
SKIP 및 TODO
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 현대적 프레임워크
Test2::V0는 Test::More의 현대적 대체품입니다 — 더 풍부한 단언문, 더 나은 진단 기능 및 확장성을 제공합니다.
왜 Test2인가?
- hash/array 빌더를 이용한 뛰어난 깊은 비교
- 실패 시 더 나은 진단 출력
- 더 깔끔한 스코핑을 가진 subtests
- Test2::Tools::* 플러그인을 통한 확장성
- Test::More 테스트와의 하위 호환성
빌더를 이용한 깊은 비교
use v5.36;
use Test2::V0;
is(
$user->to_hash,
hash {
field name => 'Alice';
field email => match(qr/\@example\.com$/);
field age => validator(sub { $_ >= 18 });
etc();
},
'user has expected fields'
);
is(
$result,
array {
item 'first';
item match(qr/^second/);
item DNE();
},
'result matches expected list'
);
is(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'has all required tags regardless of order'
);
Subtests
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;
Test2를 이용한 예외 테스트
use v5.36;
use Test2::V0;
like(
dies { divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
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;
테스트 조직화 및 prove
디렉토리 구조
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 명령어
prove -l t/
prove -lv t/
prove -lv t/unit/user.t
prove -lr t/
prove -lr -j8 t/
prove -l --state=failed t/
prove -l --color --timer t/
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
.proverc 설정
-l
--color
--timer
-r
-j4
--state=save
Fixtures 및 Setup/Teardown
Subtest 격리
use v5.36;
use Test2::V0;
use File::Temp qw(tempdir);
use Path::Tiny;
subtest 'file processing' => sub {
my $dir = tempdir(CLEANUP => 1);
my $file = path($dir, 'input.txt');
$file->spew_utf8("line1\nline2\nline3\n");
my $result = process_file("$file");
is($result->{line_count}, 3, 'counts lines');
};
공유 테스트 헬퍼 (Shared Test Helpers)
재사용 가능한 헬퍼를 t/lib/TestHelper.pm에 두고 use lib 't/lib'로 로드하십시오. create_test_db(), create_temp_dir(), fixture_path()와 같은 팩토리 함수를 Exporter를 통해 내보냅니다.
모킹 (Mocking)
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'mock external API' => sub {
my $mock = Test::MockModule->new('MyApp::API');
$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 객체를 위해서는 Test::MockObject를 사용하여 ->mock()으로 주입 가능한 테스트 더블을 생성하고 ->called_ok()로 호출을 확인하십시오.
Devel::Cover를 이용한 커버리지
커버리지 실행
cover -test
perl -MDevel::Cover -Ilib t/unit/user.t
cover
cover -report html
open cover_db/coverage.html
cover -test -report text | grep 'Total'
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
통합 테스트 (Integration Testing)
데이터베이스 테스트에는 인메모리 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;
모범 사례
권장 사항 (DO)
- TDD 준수: 구현 전에 테스트를 먼저 작성하십시오 (red-green-refactor)
- Test2::V0 사용: 현대적인 단언문, 더 나은 진단 기능 제공
- subtests 사용: 관련 단언문을 그룹화하고 상태를 격리하십시오
- 외부 의존성 모킹: 네트워크, 데이터베이스, 파일 시스템 등
prove -l 사용: @INC에 항상 lib/를 포함하십시오
- 테스트 이름을 명확하게 지정:
'잘못된 비밀번호로 사용자 로그인 시 실패'
- 엣지 케이스 테스트: 빈 문자열, undef, zero, 경계값 등
- 80% 이상의 커버리지 목표: 비즈니스 로직 경로에 집중하십시오
- 테스트 속도 유지: I/O 모킹, 인메모리 데이터베이스 사용
금지 사항 (DON'T)
- 구현을 테스트하지 마십시오: 내부 구조가 아니라 동작과 출력을 테스트하십시오
- subtests 간에 상태를 공유하지 마십시오: 각 subtest는 독립적이어야 합니다
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 망각
use Test2::V0;
is(1, 1, 'works');
use Test2::V0;
is(1, 1, 'works');
done_testing;
-l 플래그 누락
prove t/unit/user.t
prove -l t/unit/user.t
과도한 모킹 (Over-Mocking)
테스트 중인 코드가 아니라 의존성을 모킹하십시오. 테스트가 단지 모킹한 대로 반환되는지만 확인한다면 아무것도 테스트하지 않는 것입니다.
테스트 오염 (Test Pollution)
테스트 간에 상태가 유출되는 것을 방지하기 위해 subtests 내부에서는 our가 아닌 my 변수를 사용하십시오.
기억하십시오: 테스트는 여러분의 안전망입니다. 빠르고 집중적이며 독립적으로 유지하십시오. 새 프로젝트에는 Test2::V0를, 실행에는 prove를, 책임성 확인에는 Devel::Cover를 사용하십시오.