如何在不创建测试类的情况下测试Perl角色

最近我一直在开发一个游戏引擎,它使用组合模式来处理演员。我使用Role::Tiny来创建角色。Role::Tiny非常方便,因为它允许你使用角色与原生OO Perl,而不必承诺使用像Moose这样的完整对象系统。一个典型的角色看起来像这样

package March::Attribute::Id;
use 5.020;
use Role::Tiny;
use feature 'signatures';
no warnings 'experimental';

sub id ($self)
{
    $self->{id};
}

1;

这个角色所做的只是返回消费类的id属性(是的,我在整个过程中都使用了签名)。我想为这个角色编写单元测试,但我不想创建一个测试类来测试这个角色。那么如何从一个没有构造函数的包中构造一个对象呢?答案是使用你的测试文件中的bless

use strict;
use warnings;
use Test::More;

my $self = bless { id => 5 }, 'March::Attribute::Id';

BEGIN { use_ok 'March::Attribute::Id' }

is $self->id, 5, 'id()';

done_testing();

此代码通过将包名赋予角色来创建一个名为$self的对象。它为id属性添加了一个键值对,然后测试角色的id方法是否返回正确的id值。我可以使用prove执行测试

$ prove -vl t/Attribute/Id.t 
t/Attribute/Id.t .. 
ok 1 - use March::Attribute::Id;
ok 2 - id()
1..2
ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.04 CPU)
Result: PASS

这是我想要测试的另一个角色

package March::Attribute::Direction;
use 5.020;
use Role::Tiny;
use feature 'signatures';
no warnings 'experimental';
use March::Game;
use March::Msg;

requires 'id';

sub direction ($self, $new_direction = 0)
{
    if ($new_direction && $new_direction->isa('Math::Shape::Vector'))
    {
        $self->{direction} = $new_direction;

        # publish direction to game queue
        March::Game->publish(
            March::Msg->new(__PACKAGE__, $self->id, $new_direction)
        );
    }
    $self->{direction};
}

1;

这个角色获取和设置消费类的方向向量。测试这个角色的挑战在于它需要消费类实现一个id方法。Role::Tiny的requires函数是一个确保消费类满足角色要求的绝佳方式。但我们如何在不创建具有id子程序的实时类的情况下进行测试呢?我做的事情是在测试文件中声明所需的子程序

use strict;
use warnings;
use Test::More;
use Math::Shape::Vector;

# create an object
my $self = bless { direction => Math::Shape::Vector->new(1, 2) 
                  }, 'March::Attribute::Direction';

# add required sub
sub March::Attribute::Direction::id { 107 };

BEGIN { use_ok 'March::Attribute::Direction' }

is $self->direction->{x}, 1, 'Check direction x is 1';
is $self->direction->{y}, 2, 'Check direction y is 2';
ok $self->direction( Math::Shape::Vector->new(1, 0) ),
    'Update direction to new vector';
is $self->direction->{x}, 1, 'Check direction x is still 1';
is $self->direction->{y}, 0, 'Check direction y is now 0';

done_testing();

神奇的行是sub March::Attribute::Direction::id { 107 };,它将子程序添加到我要测试的角色中(它只返回值107)。现在我可以再次使用prove测试direction方法

$ prove -lv t/Attribute/Direction.t 
t/Attribute/Direction.t .. 
ok 1 - use March::Attribute::Direction;
ok 2 - Check direction
ok 3 - Check direction
ok 4 - Update direction to new vector
ok 5 - Check direction
ok 6 - Check direction
1..6
ok
All tests successful.
Files=1, Tests=6,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.00 csys =  0.10 CPU)
Result: PASS

并非全是好事

我遇到的一个缺点可以用以下角色和测试文件来看到

package Data::Inspector;
use Role::Tiny;

sub inspect_data
{
    my ($self, $data);
    Data::Dumper->Dump(['Inspecting:', $data]);
}

1;

这个角色有一个名为inspect_data的方法,它只是返回传递给它的任何数据引用的转储。这是测试文件

use Test::More;
use Data::Dumper;

my $self = bless {}, 'Data::Inspector';

BEGIN { use_ok 'Data::Inspector' } 

ok $self->inspect_data({ test => 'data' });

done_testing();

和以前一样,我在测试文件中赋予角色祝福,然后继续测试inspect_data方法。这个测试文件运行,所有测试都通过。你能在这里发现问题吗?注意,Data::Inspector角色使用Data::Dumper的 Dump方法,但它没有加载Data::Dumper模块,测试文件有!这是一个问题,因为当Data::Inspector角色在其他实时代码中使用时,如果它找不到在内存中加载了Data::Dumper,它就会崩溃并燃烧。

结论

通过这个项目,我打算创建很多简单的角色,因此这种方法提供了一种轻量级的方法,可以在测试文件中测试角色,而无需为每个角色创建测试类。

我真的喜欢Role::Tiny。它很灵活:你可以创建类似特性的最小化行为,或者更进一步,创建混合(修改状态的角色)。它有诸如自动启用strict和warnings、方法修饰符和良好的文档等不错的功能。Role::Basic是另一个支持特性(通过设计)的轻量级角色模块。我怀疑随着游戏引擎的开发进一步深入,我会不会后悔使用混合方法。


这篇文章最初发布在PerlTricks.com

标签

David Farrell

David是一位专业的程序员,他经常在推特博客上分享关于代码和编程艺术的见解。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开一个issue或pull request来帮助我们。