前回のおさらい
前回は、DockerでのPerl開発環境を構築し、Test2とMooを使ってTDDの基本サイクルを体験しました。
TDDの3つのステップを覚えていますか?
- レッド:失敗するテストを先に書く
- グリーン:最小限の実装でテストを通す
- リファクタリング:テストが通った状態でコードを改善する
前回は、シンプルなPersonクラスを作り、nameアトリビュートのテストまで行いました。
今回のゴール
今回は、もう一歩踏み込んでメソッドの振る舞いをテストしていきます。同時に、Test2が提供する便利な機能を学び、より表現力豊かなテストが書けるようになることを目指します。
具体的には以下の内容を扱います。
- メソッドの戻り値をテストする
- 正規表現マッチや構造比較など、Test2の多彩な機能
- 例外のテスト方法
- サブテストによるテストの整理
それでは、始めましょう!
メソッドの振る舞いをテストする
まずは、Personクラスにgreetメソッドを追加してみます。もちろん、TDDで進めます。
greetメソッドの実装(TDDで)
ステップ1:失敗するテストを書く(レッド)
t/02-person-methods.tを作成しましょう。
1
2
3
4
5
6
7
8
9
10
11
12
|
use strict;
use warnings;
use Test2::V0 -target => 'Person';
subtest 'greet method' => sub {
my $person = Person->new(name => 'Bob');
my $greeting = $person->greet;
is $greeting, 'Hello, I am Bob!', 'greet returns correct message';
};
done_testing;
|
テストを実行してみます。
1
|
docker compose run --rm app prove -lv t/02-person-methods.t
|
当然、greetメソッドがまだ存在しないので、エラーになります。これが「レッド」の状態です。
ステップ2:最小限の実装で通す(グリーン)
lib/Person.pmにgreetメソッドを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package Person;
use Moo;
use strictures 2;
has name => (
is => 'ro',
required => 1,
);
sub greet {
my $self = shift;
return "Hello, I am " . $self->name . "!";
}
1;
|
テストを実行する
1
|
docker compose run --rm app prove -lv t/02-person-methods.t
|
再度テストを実行すると、今度は成功するはずです。これが「グリーン」です!
グリーンになったら、この変更をコミットしましょう:
1
2
|
git add lib/Person.pm t/02-person-methods.t
git commit -m "Add greet method to Person class"
|
引数を受け取るメソッドのテスト
次は、引数を受け取るメソッドをテストしてみましょう。greet_toメソッドを追加します。
テストを先に書く
1
2
3
4
5
6
|
subtest 'greet_to method with argument' => sub {
my $person = Person->new(name => 'Bob');
my $greeting = $person->greet_to('Alice');
is $greeting, 'Hello Alice, I am Bob!', 'greet_to works with argument';
};
|
実装する
1
2
3
4
|
sub greet_to {
my ($self, $target) = @_;
return "Hello $target, I am " . $self->name . "!";
}
|
テストを実行する
1
|
docker compose run --rm app prove -lv t/02-person-methods.t
|
このように、TDDでは「テストを書く→実装する」を小刻みに繰り返します。
Test2の便利な機能
ここからは、Test2が提供する様々なアサーション機能を見ていきましょう。
like / unlike(正規表現マッチ)
文字列が特定のパターンにマッチするかをテストできます。
1
2
3
4
5
6
7
8
9
10
|
use Test2::V0 -target => 'Person';
subtest 'regular expression matching' => sub {
my $person = Person->new(name => 'Charlie');
my $greeting = $person->greet;
like $greeting, qr/Hello/, 'greeting contains "Hello"';
like $greeting, qr/Charlie/, 'greeting contains person name';
unlike $greeting, qr/Goodbye/, 'greeting does not contain "Goodbye"';
};
|
likeは正規表現にマッチすることを確認し、unlikeはマッチしないことを確認します。
ok / is / isnt の使い分け
Test2には、いくつかの基本的なアサーション関数があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
subtest 'basic assertions' => sub {
my $person = Person->new(name => 'Dave');
# ok: 真偽値のテスト
ok $person, 'person object exists';
ok $person->name, 'person has a name';
# is: 等価性のテスト(厳密な比較)
is $person->name, 'Dave', 'name is exactly "Dave"';
# isnt: 非等価性のテスト
isnt $person->name, 'Eve', 'name is not "Eve"';
};
|
使い分けのポイント:
ok:真偽値だけを確認したい時
is:具体的な値が一致するか確認したい時
isnt:値が異なることを確認したい時
array / hash(構造の比較)
配列やハッシュの構造を比較するときに便利な機能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
use Test2::V0;
subtest 'array and hash comparison' => sub {
my @names = ('Alice', 'Bob', 'Charlie');
# 配列の比較
is \@names, ['Alice', 'Bob', 'Charlie'], 'array matches expected values';
my %person_info = (
name => 'Alice',
age => 30,
);
# ハッシュの比較
is \%person_info, {
name => 'Alice',
age => 30,
}, 'hash matches expected structure';
};
|
リファレンスとして渡すことで、構造全体を一度に比較できます。
object(オブジェクトの検証)
オブジェクトの型やメソッドの存在を確認できます。
1
2
3
4
5
6
7
8
9
10
|
subtest 'object validation' => sub {
my $person = Person->new(name => 'Frank');
# オブジェクトの型を確認
isa_ok $person, [ 'Person' ], 'person is a Person object';
# メソッドの存在を確認
can_ok $person, [ 'greet' ], 'person can greet';
can_ok $person, [ 'greet_to' ], 'person can greet_to';
};
|
subtestを追加する感じで問題ありません。
isa_okは継承関係の確認に、can_okはメソッドの存在確認に使います。
例外のテスト
実際のアプリケーションでは、エラー処理も重要です。Test2では、例外のテストも簡単に書けます。
dies(例外が発生することを確認)
まず、年齢を設定するメソッドで、負の値を拒否する機能を追加してみましょう。
テストから書く
1
2
3
4
5
6
7
8
9
10
11
12
|
use Test2::V0;
subtest 'age validation' => sub {
my $person = Person->new(name => 'Grace');
# 正常な値は受け入れる
ok lives { $person->set_age(25) }, 'valid age is accepted';
is $person->age, 25, 'age is set correctly';
# 負の値は例外を投げる
ok dies { $person->set_age(-5) }, 'negative age throws exception';
};
|
実装する
1
2
3
4
5
6
7
8
9
10
|
has age => (
is => 'rw',
predicate => 1,
);
sub set_age {
my ($self, $age) = @_;
die "Age must be non-negative" if $age < 0;
$self->age($age);
}
|
lives(例外が発生しないことを確認)
逆に、例外が発生しないことを確認する場合はlivesを使います。
1
2
3
4
5
6
|
subtest 'valid operations do not throw' => sub {
my $person = Person->new(name => 'Henry');
ok lives { $person->greet }, 'greet does not throw';
ok lives { $person->greet_to('Ivy') }, 'greet_to does not throw';
};
|
例外メッセージの検証
例外のメッセージ内容まで確認したい場合は、likeと組み合わせます。
1
2
3
4
5
6
7
8
9
|
subtest 'exception message validation' => sub {
my $person = Person->new(name => 'Jack');
like(
dies { $person->set_age(-10) },
qr/must be non-negative/,
'exception message contains expected text'
);
};
|
これで、例外の発生だけでなく、その内容まで検証できます。
サブテストで整理する
テストが増えてくると、整理が重要になります。subtestを使うと、論理的にグループ化できます。
subtest の使い方
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
|
use strict;
use warnings;
use Test2::V0 -target => 'Person';
subtest 'Person creation' => sub {
my $person = Person->new(name => 'Kate');
isa_ok $person, 'Person';
is $person->name, 'Kate', 'name is set correctly';
};
subtest 'Greeting methods' => sub {
my $person = Person->new(name => 'Leo');
subtest 'basic greet' => sub {
my $greeting = $person->greet;
like $greeting, qr/Hello/, 'contains greeting';
like $greeting, qr/Leo/, 'contains name';
};
subtest 'greet with target' => sub {
my $greeting = $person->greet_to('Mia');
like $greeting, qr/Hello Mia/, 'addresses target';
like $greeting, qr/I am Leo/, 'introduces self';
};
};
subtest 'Age validation' => sub {
my $person = Person->new(name => 'Nina');
ok lives { $person->set_age(30) }, 'accepts valid age';
ok dies { $person->set_age(-1) }, 'rejects negative age';
};
done_testing;
|
テストの可読性向上
サブテストを使うことで、以下のメリットがあります。
- 構造が明確:どの機能をテストしているか一目瞭然
- 失敗箇所の特定が容易:サブテストの名前でエラー箇所がすぐわかる
- メンテナンスしやすい:関連するテストがまとまっている
論理的なグループ化
サブテストは、以下のような観点でグループ化できます。
- 機能別:「作成」「更新」「削除」など
- シナリオ別:「正常系」「異常系」など
- 対象別:「アトリビュート」「メソッド」「例外」など
テストファイルが成長してきたら、積極的にサブテストで整理しましょう。
まとめと次回予告
今回学んだこと
今回は、Test2の豊富な機能を使って、より実践的なテストを書く方法を学びました。
- メソッドのテスト:戻り値や引数を持つメソッドのテスト方法
- Test2の多彩な機能:
like、array、hash、isa_ok、can_okなど
- 例外のテスト:
diesとlivesによる例外処理の検証
- サブテスト:テストの論理的なグループ化と可読性向上
Test2を使えば、表現力豊かで読みやすいテストが書けることを実感できたのではないでしょうか。
TDDの効果を実感する
TDDで開発すると、以下のような効果があります。
- 仕様が明確になる:テストが要求仕様のドキュメントになる
- 安心してリファクタリングできる:テストが壊れていないことを保証
- バグが減る:実装前にテストケースを考えることで、漏れが減る
最初は慣れないかもしれませんが、小さく始めて徐々に慣れていきましょう。
次回予告
次回は「MooによるTDD講座 #3 - リファクタリングとロールの活用」として、以下の内容を扱います。
- テストが通った状態での安全なリファクタリング
- Mooのロール(役割)を使ったコードの再利用
- 複数のクラス間でのコード共有
- テスタビリティを高める設計
コードを整理しながら、Mooの真価を発揮させる方法を学びます。お楽しみに!