perl-testing

perl-testing

熱門

使用 Test2::V0、Test::More、prove 執行器、模擬、Devel::Cover 覆蓋率以及 TDD 方法的 Perl 測試模式。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
perl-testing
description

使用 Test2::V0、Test::More、prove 執行器、模擬、Devel::Cover 覆蓋率以及 TDD 方法的 Perl 測試模式。

Perl 測試模式

使用 Test2::V0、Test::More、prove 和 TDD 方法的 Perl 應用程式全面測試策略。

何時啟用

  • 撰寫新的 Perl 程式碼(遵循 TDD:紅、綠、重構)
  • 為 Perl 模組或應用程式設計測試套件
  • 審查 Perl 測試覆蓋率
  • 設定 Perl 測試基礎設施
  • 從 Test::More 遷移測試到 Test2::V0
  • 除錯失敗的 Perl 測試

TDD 工作流程

始終遵循 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 — 在測試保持綠燈的情況下改進
# 執行:prove -lv t/unit/calculator.t

Test::More 基礎

標準的 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;

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 建構器進行優越的深度比較
  • 失敗時更好的診斷輸出
  • 子測試具有更乾淨的作用域
  • 透過 Test2::Tools::* 外掛可擴展
  • 向後相容 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;

使用 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/

# 平行執行(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

.proverc 設定

-l
--color
--timer
-r
-j4
--state=save

Fixtures 與 Setup/Teardown

子測試隔離

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');

    # 自動清理(CLEANUP => 1)
};

共享測試輔助工具

將可重複使用的輔助工具放在 t/lib/TestHelper.pm 中,並使用 use lib 't/lib' 載入。透過 Exporter 匯出工廠函式,例如 create_test_db()create_temp_dir()fixture_path()

模擬

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 離開作用域時,模擬會自動還原
};

# 壞:猴子修補而不還原
# *MyApp::API::fetch_user = sub { ... };  # 絕對不要 — 會洩漏到其他測試

對於輕量級的模擬物件,使用 Test::MockObject 來建立可注入的測試替身,並使用 ->mock()->called_ok() 驗證呼叫。

使用 Devel::Cover 進行覆蓋率

執行覆蓋率

# 基本覆蓋率報告
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;

最佳實踐

應該做的事

  • 遵循 TDD:在實作之前撰寫測試(紅-綠-重構)
  • 使用 Test2::V0:現代斷言,更好的診斷
  • 使用子測試:分組相關斷言,隔離狀態
  • 模擬外部依賴:網路、資料庫、檔案系統
  • 使用 prove -l:始終將 lib/ 包含在 @INC
  • 清楚命名測試'user login with invalid password fails'
  • 測試邊界情況:空字串、undef、零、邊界值
  • 目標 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

# 壞:測試檔案執行但未驗證所有測試已執行
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

# 好:將 lib/ 包含在 @INC 中
prove -l t/unit/user.t

過度模擬

模擬依賴,而不是被測試的程式碼。如果你的測試只驗證模擬回傳了你告訴它的內容,那它什麼也沒測試。

測試污染

在子測試內部使用 my 變數 — 絕對不要用 our — 以防止狀態在測試之間洩漏。

記住:測試是你的安全網。保持快速、專注且獨立。新專案使用 Test2::V0,使用 prove 執行,並使用 Devel::Cover 確保責任歸屬。