Test2フレームワークとは
Test2は、Perlのテストエコシステムにおける次世代のテストフレームワークです。長年親しまれてきたTest::Moreの後継として開発され、より柔軟で拡張性の高い設計になっています。
Test::MoreはTest::Builderという内部APIに依存していましたが、これは20年以上前の設計であり、モダンなテストのニーズに対応しきれなくなっていました。Test2はこの問題を解決するため、クリーンなアーキテクチャで一から設計されています。
Test::Moreとの主な違い
- モジュール性: Test2はコアとプラグインが明確に分離されている
- 拡張性: カスタムアサーションやフォーマッタを簡単に作成できる
- 並行実行: Test2::Harness::UIによる並列テスト実行のサポート
- 詳細な診断: テスト失敗時により詳しい情報を提供
- 後方互換性: Test::Moreのコードもそのまま動作する(Test2の上で実装されている)
Test2::V0を使ってみよう
Test2を使い始める最も簡単な方法は、Test2::V0モジュールを使うことです。これは必要な機能がすべてバンドルされた「バッテリー同梱」パッケージです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
use Test2::V0;
# 基本的なテスト
ok(1, 'this passes');
ok(0, 'this fails');
# 等価性のテスト
is(1 + 1, 2, 'math works');
isnt('foo', 'bar', 'strings differ');
# パターンマッチ
like('hello world', qr/world/, 'pattern matches');
unlike('hello world', qr/goodbye/, 'pattern does not match');
# 配列とハッシュの比較
is([1, 2, 3], [1, 2, 3], 'arrays match');
is({a => 1, b => 2}, {a => 1, b => 2}, 'hashes match');
# テストの終了
done_testing;
|
Test2::V0が提供する主な関数
Test2::V0をuseすると、以下の関数群が自動的にインポートされます:
ok, is, isnt, like, unlike - 基本的なアサーション
dies, lives, warns, no_warnings - 例外とwarningのテスト
can_ok, isa_ok, does_ok - オブジェクト指向のテスト
subtest - サブテストの作成
skip, todo - 条件付きテストとTODOマーカー
done_testing - テスト終了の明示
実践的なテストコード
モジュールのテスト
実際のモジュールをテストする例を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# lib/Calculator.pm
package Calculator;
use strict;
use warnings;
sub new {
my $class = shift;
return bless {}, $class;
}
sub add {
my ($self, $a, $b) = @_;
return $a + $b;
}
sub divide {
my ($self, $a, $b) = @_;
die "Division by zero" if $b == 0;
return $a / $b;
}
1;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
# t/calculator.t
use Test2::V0;
# モジュールの読み込みテスト
require_ok('Calculator');
# オブジェクト生成のテスト
my $calc = Calculator->new;
isa_ok($calc, 'Calculator', 'object created');
# メソッドの存在確認
can_ok($calc, [qw(add divide)]);
# 加算のテスト
subtest 'addition tests' => sub {
is($calc->add(2, 3), 5, '2 + 3 = 5');
is($calc->add(-1, 1), 0, '-1 + 1 = 0');
is($calc->add(0, 0), 0, '0 + 0 = 0');
};
# 除算のテスト
subtest 'division tests' => sub {
is($calc->divide(10, 2), 5, '10 / 2 = 5');
is($calc->divide(7, 2), 3.5, '7 / 2 = 3.5');
# 例外のテスト
like(
dies { $calc->divide(10, 0) },
qr/Division by zero/,
'dies on division by zero'
);
};
done_testing;
|
サブテストの活用
サブテストを使うことで、関連するテストをグループ化し、可読性を向上させることができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
use Test2::V0;
subtest 'string operations' => sub {
my $str = "Hello, World";
like($str, qr/Hello/, 'contains Hello');
like($str, qr/World/, 'contains World');
is(length($str), 12, 'correct length');
};
subtest 'array operations' => sub {
my @arr = (1, 2, 3, 4, 5);
is(scalar(@arr), 5, 'has 5 elements');
is($arr[0], 1, 'first element is 1');
is($arr[-1], 5, 'last element is 5');
};
done_testing;
|
TodoとSkip
テスト駆動開発では、まだ実装していない機能のテストを書くことがあります。そのような場合にtodoが役立ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
use Test2::V0;
# まだ実装していない機能
todo 'not yet implemented' => sub {
my $result = future_function();
is($result, 42, 'should return 42');
};
# 条件付きでテストをスキップ
SKIP: {
skip 'requires database connection', 3 unless $ENV{TEST_DB};
my $dbh = connect_to_database();
ok($dbh, 'connected to database');
# ... more database tests
}
done_testing;
|
Test2::V0では、より明示的な方法でスキップを記述できます:
1
2
3
4
5
6
7
8
9
10
|
use Test2::V0;
if ($^O eq 'MSWin32') {
skip_all 'Unix-specific tests';
}
# Unixでのみ実行されるテスト
ok(-e '/etc/passwd', 'passwd file exists');
done_testing;
|
モックとスタブ
外部依存をモックすることで、単体テストを独立させることができます。Test2エコシステムにはTest2::Mockが用意されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
use Test2::V0;
use Test2::Mock;
# モックオブジェクトの作成
my $mock = Test2::Mock->new(
class => 'LWP::UserAgent',
override => [
get => sub {
my ($self, $url) = @_;
# モックレスポンスを返す
return HTTP::Response->new(
200, 'OK',
['Content-Type' => 'text/plain'],
'Mocked response'
);
},
],
);
# モックを使ったテスト
require MyApp::WebClient;
my $client = MyApp::WebClient->new;
my $response = $client->fetch('http://example.com');
is($response->code, 200, 'got 200 response');
is($response->content, 'Mocked response', 'got mocked content');
done_testing;
|
より簡単なモック例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
use Test2::V0;
use Test2::Mock;
{
package MyModule;
sub get_time { return time }
sub greet {
my ($self, $name) = @_;
return "Hello, $name at " . get_time();
}
}
# timeをモック
my $mock = Test2::Mock->new(
class => 'MyModule',
override => [
get_time => sub { 1234567890 },
],
);
my $obj = bless {}, 'MyModule';
like($obj->greet('Alice'), qr/Alice at 1234567890/, 'time is mocked');
done_testing;
|
高度なアサーション
Test2::V0は、構造化されたデータの比較に強力な機能を提供します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
use Test2::V0;
# 複雑なデータ構造のテスト
my $user = {
name => 'Alice',
age => 30,
email => 'alice@example.com',
roles => ['user', 'admin'],
};
is(
$user,
{
name => 'Alice',
age => 30,
email => qr/\@example\.com$/,
roles => bag { item 'user'; item 'admin'; },
},
'user structure matches'
);
# 部分一致のテスト
like(
$user,
{
name => 'Alice',
# 他のフィールドは無視
},
'has expected name field'
);
done_testing;
|
array/hash/bagの使い分け
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
use Test2::V0;
# 順序も含めて完全一致
is([1, 2, 3], [1, 2, 3], 'arrays match exactly');
# 順序を無視して一致(bag)
is(
[3, 1, 2],
bag { item 1; item 2; item 3; },
'elements match regardless of order'
);
# ハッシュのキーと値
is(
{a => 1, b => 2},
hash {
field a => 1;
field b => 2;
end; # 他のフィールドがないことを確認
},
'hash matches exactly'
);
done_testing;
|
Webアプリケーションのテスト
MojoliciousアプリケーションをTest2でテストする例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# lib/MyApp.pm
package MyApp;
use Mojo::Base 'Mojolicious', -signatures;
sub startup ($self) {
my $r = $self->routes;
$r->get('/')->to(cb => sub ($c) {
$c->render(text => 'Hello, World!');
});
$r->get('/api/user/:id')->to(cb => sub ($c) {
my $id = $c->param('id');
$c->render(json => {id => $id, name => "User $id"});
});
}
1;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# t/myapp.t
use Test2::V0;
use Test::Mojo;
# アプリケーションのテスト
my $t = Test::Mojo->new('MyApp');
# ルートのテスト
$t->get_ok('/')
->status_is(200)
->content_is('Hello, World!');
# JSONレスポンスのテスト
$t->get_ok('/api/user/123')
->status_is(200)
->json_is('/id', 123)
->json_is('/name', 'User 123');
done_testing;
|
Test2::V0と組み合わせることもできます:
1
2
3
4
5
6
7
8
9
10
11
12
|
use Test2::V0;
use Test::Mojo;
my $t = Test::Mojo->new('MyApp');
subtest 'homepage' => sub {
my $tx = $t->get_ok('/')->tx;
is($tx->res->code, 200, 'status is 200');
like($tx->res->body, qr/Hello/, 'contains greeting');
};
done_testing;
|
カバレッジ測定
テストのカバレッジを測定するには、Devel::Coverを使用します。
1
2
3
4
5
6
7
8
9
10
11
|
# cpanfile に追加
requires 'Devel::Cover';
# インストール
cpanm --installdeps .
# カバレッジ測定付きでテスト実行
cover -test -ignore_re '^t/'
# HTMLレポート生成
cover -report html_basic
|
カバレッジレポートはcover_db/coverage.htmlに生成されます。
特定のディレクトリのみを対象にする場合:
1
2
3
4
|
cover -test \
-ignore_re '^t/' \
-ignore_re '^local/' \
-select '^lib/'
|
CI/CDでの活用
GitHub Actionsでの設定例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
# .github/workflows/test.yml
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
perl-version: ['5.32', '5.34', '5.36', '5.38']
steps:
- uses: actions/checkout@v3
- name: Set up Perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl-version }}
- name: Install dependencies
run: |
cpanm --quiet --notest --installdeps .
- name: Run tests
run: prove -lvr t/
- name: Coverage report
if: matrix.perl-version == '5.38'
run: |
cpanm --quiet --notest Devel::Cover Devel::Cover::Report::Coveralls
cover -test -report coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
prove コマンドの活用
Test2は標準的なproveコマンドで実行できます:
1
2
3
4
5
6
7
8
9
10
11
|
# 全テストを実行
prove -lvr t/
# 並列実行(4プロセス)
prove -lvr -j4 t/
# 特定のテストのみ
prove -lv t/calculator.t
# 失敗したテストのみ再実行
prove -lvr --state=failed t/
|
ベストプラクティス
1. Test2::V0を使う
古いTest::Moreではなく、Test2::V0を使いましょう。機能が豊富で、後方互換性もあります。
1
2
3
4
5
|
# Good
use Test2::V0;
# Old style (still works, but less features)
use Test::More;
|
2. サブテストでグループ化
関連するテストはサブテストでまとめることで、可読性が向上します。
1
2
3
4
5
6
7
|
subtest 'validation tests' => sub {
# related tests here
};
subtest 'edge cases' => sub {
# edge case tests here
};
|
3. 明示的なテスト数よりdone_testing
Test2::V0ではdone_testingを使うのが推奨されます。
1
2
3
4
5
6
7
|
# Good
use Test2::V0;
# ... tests ...
done_testing;
# Avoid (unless you have a specific reason)
use Test2::V0 tests => 10;
|
4. 意味のあるテスト名
テストには必ず説明的な名前を付けましょう。
1
2
3
4
5
|
# Good
is($user->age, 30, 'user age is correct');
# Bad
is($user->age, 30);
|
5. 例外テストはdies/livesを使う
eval を使った手動チェックではなく、Test2::V0の機能を活用しましょう。
1
2
3
4
5
6
7
8
9
10
|
# Good
like(
dies { dangerous_operation() },
qr/expected error/,
'dies with expected message'
);
# Avoid
eval { dangerous_operation() };
like($@, qr/expected error/);
|
6. warningのテスト
warningも適切にテストしましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
use Test2::V0;
# warningが出ることを確認
like(
warns { deprecated_function() },
qr/deprecated/,
'warns about deprecation'
);
# warningが出ないことを確認
ok(
no_warnings { modern_function() },
'no warnings from modern function'
);
done_testing;
|
7. モックは必要最小限に
モックは便利ですが、使いすぎると実際の動作と乖離します。本当に必要な場合のみ使用しましょう。
1
2
|
# モックが必要な例:外部API、データベース、現在時刻
# モックが不要な例:自分のモジュールの内部関数
|
テスト駆動開発(TDD)の実践
Test2を使ったTDDのワークフロー例:
ステップ1: テストを先に書く
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# t/user.t
use Test2::V0;
require_ok('User');
subtest 'user creation' => sub {
my $user = User->new(name => 'Alice', email => 'alice@example.com');
isa_ok($user, 'User');
is($user->name, 'Alice', 'name is set');
is($user->email, 'alice@example.com', 'email is set');
};
subtest 'email validation' => sub {
like(
dies { User->new(name => 'Bob', email => 'invalid') },
qr/invalid email/i,
'rejects invalid email'
);
};
done_testing;
|
ステップ2: 最小限の実装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# lib/User.pm
package User;
use strict;
use warnings;
sub new {
my ($class, %args) = @_;
# メールアドレスの簡易バリデーション
if ($args{email} && $args{email} !~ /\@/) {
die "Invalid email format";
}
return bless \%args, $class;
}
sub name { $_[0]->{name} }
sub email { $_[0]->{email} }
1;
|
ステップ3: テストを実行
ステップ4: リファクタリング
テストが通ったら、コードを改善していきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# lib/User.pm - improved version
package User;
use strict;
use warnings;
use Carp qw(croak);
sub new {
my ($class, %args) = @_;
croak "name is required" unless $args{name};
croak "email is required" unless $args{email};
_validate_email($args{email});
return bless \%args, $class;
}
sub _validate_email {
my $email = shift;
croak "Invalid email format: $email"
unless $email =~ /^[^@]+@[^@]+\.[^@]+$/;
}
sub name { $_[0]->{name} }
sub email { $_[0]->{email} }
1;
|
テストを追加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# t/user.t に追加
subtest 'required fields' => sub {
like(
dies { User->new(email => 'alice@example.com') },
qr/name is required/,
'name is required'
);
like(
dies { User->new(name => 'Alice') },
qr/email is required/,
'email is required'
);
};
|
まとめ
Test2フレームワークは、Perlのテストエコシステムにおける新しいスタンダードです。主な利点は:
- 使いやすさ: Test2::V0で必要な機能がすべて揃う
- 強力: 複雑なデータ構造の比較も簡単
- 拡張性: カスタムアサーションやフォーマッタを追加できる
- 互換性: 既存のTest::Moreコードもそのまま動作
- モダン: 並列実行やCI/CDとの統合が容易
テスト駆動開発を実践することで、より堅牢で保守しやすいコードを書くことができます。Test2はその強力な味方となるでしょう。
さらに学ぶには
Happy Testing! 🎄