Featured image of post Test2フレームワーク入門

Test2フレームワーク入門

Perl の次世代テストフレームワーク Test2 と Test2::V0 の基本を解説し、実践的なテスト例を紹介します。

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: テストを実行

1
prove -lv t/user.t

ステップ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! 🎄

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。