经过长时间的聊天讨论,我提出了以下方法来进行单元测试,以及轻松重构实际代码以使事情变得更容易。
变化我做
我已经重构的代码从模板创建错误消息不使用Template的方式,因为它是清楚your previous question,这是一个有点矫枉过正。
它现在使用sprintf
和Timeout after %s seconds
这样的简单模式。我在整个例子中都使用了%s
,因为从来没有任何类型的检查,但它当然可以添加。此消息的参数作为从第二个参数开始的键/值对列表传递给构造函数。
my $e = Error->new(CONSTANT, foo => 'bar');
的例子ErrorLibrary
的第一个参数CONSTANT
仍然是你的错误库。我已经包括了以下简化的例子。
package ErrorList;
use strict;
use warnings;
use parent 'Exporter';
use constant {
ERROR_WIFI_CABLE_TOO_SHORT => {
category => 'Layer 1',
template => 'A WiFi cable of %s meters is too short.',
context => [qw(length)],
fatal => 1,
wiki_page => 'http://example.org',
},
ERROR_CABLE_HAS_WRONG_COLOR => {
category => 'Layer 1',
template => 'You cannot connect to %s using a %s cable.',
context => [qw(router color)],
fatal => 1,
wiki_page => 'http://example.org',
},
ERROR_I_AM_A_TEAPOT => {
category => 'Layer 3',
template => 'The device at %s is a teapot.',
context => [qw(ip)],
fatal => 0,
wiki_page => 'http://example.org',
},
};
our @EXPORT = qw(
ERROR_WIFI_CABLE_TOO_SHORT
ERROR_CABLE_HAS_WRONG_COLOR
ERROR_I_AM_A_TEAPOT
);
our @EXPORT_OK = qw(ERROR_WIFI_CABLE_TOO_SHORT);
的上下文是与预计在施工该密钥的列表的数组引用。
的重构(简体)Error类
这一类包括POD来解释它做什么。重要的方法是构造函数message
和stringify
。
package Error;
use strict;
use warnings;
=head1 NAME
Error - A handy error class
=head1 SYNOPSIS
use Error;
use ErrorList 'ERROR_WIFI_CABLE_TOO_SHORT';
my $e = Error->new(
ERROR_WIFI_CABLE_TOO_SHORT,
timeout => 30,
switch_ip => '127.0.0.1'
);
die $e->stringify;
=head1 DESCRIPTION
This class can create objects from a template and stringify them into a
log-compatible pattern. It makes sense to use it together
with L<ErrorList>.
=head1 METHODS
=head2 new($error, %args)
The constructor takes the error definition and a list of key/value pairs
with context information as its arguments.
...
=cut
sub new {
my ($class, $error, %args) = @_;
# initialize with the error data
my $self = $error;
# check required arguments...
foreach my $key (@{ $self->{context} }) {
die "$key is required" unless exists $args{$key};
# ... and take the ones we need
$self->{args}->{$key} = $args{$key}; # this could have a setter
}
return bless $self, $class;
}
=head2 category
This is the accessor for the category.
=cut
sub category {
return $_[0]->{category};
}
=head2 template
This is the accessor for the template.
=cut
sub template {
return $_[0]->{template};
}
=head2 fatal
This is the accessor for whether the error is fatal.
=cut
sub is_fatal {
return $_[0]->{fatal};
}
=head2 wiki_page
This is the accessor for the wiki_page.
=cut
sub wiki_page {
return $_[0]->{wiki_page};
}
=head2 context
This is the accessor for the context. The context is an array ref
of hash key names that are required as context arguments at construction.
=cut
sub context {
return $_[0]->{context};
}
=head2 category
This is the accessor for the args. The args are a hash ref of context
arguments that are passed in as a list at construction.
=cut
sub args {
return $_[0]->{args};
}
=head2 message
Builds the message string from the template.
=cut
sub message {
my ($self) = @_;
return sprintf $self->template,
map { $self->args->{$_} } @{ $self->context };
}
=head2 stringify
Stringifies the error to a log message, including the message,
category and wiki_page.
=cut
sub stringify {
my ($self) = @_;
return sprintf qq{%s : %s\nMore info: %s}, $self->category,
$self->message, $self->wiki_page;
}
=head1 AUTHOR
simbabque (some guy on StackOverflow)
=cut
本单位实际测试
我们测试,它的行为和数据进行区分是很重要的。 行为包括代码中定义的所有访问器,以及更多有趣的子项,如new
,message
和stringify
。
我为这个例子创建的测试文件的第一部分包括这些。它会创建一个假错误结构$example_error
,并使用它来检查构造函数是否可以处理正确的参数,缺失或超出的参数,访问器返回正确的内容,并且message
和stringify
都会创建正确的内容。
请记住,这些测试主要是更改代码时的安全网(尤其是在几个月后)。如果你不小心改变了错误的地方,测试将会失败。
package main; # something like 01_foo.t
use strict;
use warnings;
use Test::More;
use Test::Exception;
use LWP::Simple 'head';
subtest 'Functionality of Error' => sub {
my $example_error = {
category => 'Connection Error',
template => 'Could not ping switch %s in %s seconds.',
context => [qw(switch_ip timeout)],
fatal => 1,
wiki_page => 'http://example.org',
};
# happy case
{
my $e = Error->new(
$example_error,
timeout => 30,
switch_ip => '127.0.0.1'
);
isa_ok $e, 'Error';
can_ok $e, 'category';
is $e->category, 'Connection Error',
q{... and it returns the correct value};
can_ok $e, 'template';
is $e->template, 'Could not ping switch %s in %s seconds.',
q{... and it returns the correct values};
can_ok $e, 'context';
is_deeply $e->context, [ 'switch_ip', 'timeout' ],
q{... and it returns the correct values};
can_ok $e, 'is_fatal';
ok $e->is_fatal, q{... and it returns the correct values};
can_ok $e, 'message';
is $e->message, 'Could not ping switch 127.0.0.1 in 30 seconds.',
q{... and the message is correct};
can_ok $e, 'stringify';
is $e->stringify,
"Connection Error : Could not ping switch 127.0.0.1 in 30 seconds.\n"
. "More info: http://example.org",
q{... and stringify contains the right message};
}
# not enough arguments
throws_ok(sub { Error->new($example_error, timeout => 1) },
qr/switch_ip/, q{Creating without switch_ip dies});
# too many arguments
lives_ok(
sub {
Error->new(
$example_error,
timeout => 1,
switch_ip => 2,
foo => 3
);
},
q{Creating with too many arguments lives}
);
};
有一些特定的测试案例丢失。如果您使用像Devel::Cover这样的度量工具,值得注意的是全覆盖并不意味着涵盖所有可能的情况。
测试您的错误数据质量
现在的第二部分,是值得覆盖在这个例子是在ErrorLibrary错误模板的正确性。有人可能会在以后意外混淆某些东西,或者可能会在消息中添加新的占位符,但不会添加到上下文数组中。
下面的测试代码理想情况下将放置在它自己的文件中,并且只有在完成某个功能时才能运行,但出于说明的目的,这只是在上述代码块之后继续执行,因此两个第一级subtest
。
您问题的主要部分是关于测试用例的列表。我认为这是非常重要的。你希望你的测试代码干净,容易阅读,甚至更容易维护。测试经常作为文档加倍,没有什么比更改代码更烦人,然后试图弄清楚测试如何工作,以便更新它们。所以永远记住这一点:
测试也是生产代码!
现在让我们来看看这些错误的测试。
subtest 'Correctness of ErrorList' => sub {
# these test cases contain all the errors from ErrorList
my @test_cases = (
{
name => 'ERROR_WIFI_CABLE_TOO_SHORT',
args => {
length => 2,
},
message => 'A WiFi cable of 2 meters is too short.',
},
{
name => 'ERROR_CABLE_HAS_WRONG_COLOR',
args => {
router => 'foo',
color => 'red',
},
message => 'You cannot connect to foo using a red cable.',
},
{
name => 'ERROR_I_AM_A_TEAPOT',
args => {
ip => '127.0.0.1',
},
message => 'The device at 127.0.0.1 is a teapot.',
},
);
# use_ok 'ErrorList'; # only use this line if you have files!
ErrorList->import; # because we don't have a file ErrorList.pm
# in the file system
pass 'ErrorList used correctly'; # remove if you have files
foreach my $t (@test_cases) {
subtest $t->{name} => sub {
# because we need to use a variable to get to a constant
no strict 'refs';
# create the Error object from the test data
# will also fail if the name was not exported by ErrorList
my $e;
lives_ok(
sub { $e = Error->new(&{ $t->{name} }, %{ $t->{args} }) },
q{Error can be created});
# and see if it has the right values
is $e->message, $t->{message},
q{... and the error message is correct};
# use LWP::Simple to check if the wiki page link is not broken
ok head($e->wiki_page), q{... and the wiki page is reachable};
};
}
};
done_testing;
它基本上有一个测试用例数组,其中每个可能的错误常量由ErrorLibrary导出。它具有名称,用于加载正确的错误,并在TAP输出中标识测试用例,运行测试所需的参数以及预期的最终输出。我只包含消息以保持简短。
如果在ErrorLibrary(或删除)中修改错误模板名称而未更改文本,围绕对象实例化的lives_ok
将失败,因为该名称未导出。这是一个很好的补充。
但是,如果在没有测试用例的情况下添加新的错误,它将不会被捕获。一种方法是查看名称空间main
中的符号表,但这对于此答案的范围来说有点太高级了。
它还会用LWP::Simple对每个wiki URL执行HEAD
HTTP请求以查看它们是否可到达。这也有很好的好处,如果你在构建时运行它,它就像一个监视工具。
把它放在一起
最后,这里是TAP输出,当没有prove
运行。
# Subtest: Functionality of Error
ok 1 - An object of class 'Error' isa 'Error'
ok 2 - Error->can('category')
ok 3 - ... and it returns the correct value
ok 4 - Error->can('template')
ok 5 - ... and it returns the correct values
ok 6 - Error->can('context')
ok 7 - ... and it returns the correct values
ok 8 - Error->can('is_fatal')
ok 9 - ... and it returns the correct values
ok 10 - Error->can('message')
ok 11 - ... and the message is correct
ok 12 - Error->can('stringify')
ok 13 - ... and stringify contains the right message
ok 14 - Creating without switch_ip dies
ok 15 - Creating with too many arguments lives
1..15
ok 1 - Functionality of Error
# Subtest: Correctness of ErrorList
ok 1 - ErrorList used correctly
# Subtest: ERROR_WIFI_CABLE_TOO_SHORT
ok 1 - Error can be created
ok 2 - ... and the error message is correct
ok 3 - ... and the wiki page is reachable
1..3
ok 2 - ERROR_WIFI_CABLE_TOO_SHORT
# Subtest: ERROR_CABLE_HAS_WRONG_COLOR
ok 1 - Error can be created
ok 2 - ... and the error message is correct
ok 3 - ... and the wiki page is reachable
1..3
ok 3 - ERROR_CABLE_HAS_WRONG_COLOR
# Subtest: ERROR_I_AM_A_TEAPOT
ok 1 - Error can be created
ok 2 - ... and the error message is correct
ok 3 - ... and the wiki page is reachable
1..3
ok 4 - ERROR_I_AM_A_TEAPOT
1..4
ok 2 - Correctness of ErrorList
1..2
请说明问题所在。你在问如何构建单元测试以避免代码重复?你正在谈论'流程',从你以前的问题中我相信这是一种方法。为什么会进入单元测试? – simbabque
没有抱歉,我只是想知道如何设置我的测试,以便能够测试一个错误,我可以使用for循环或类似的方式运行多个错误的测试。我并不是故意将这个词汇加入其中。我只是认为我应该用'data = [input => UNABLE_TO_PING_SWITCH_ERROR,output =>“无法在30秒内ping开关192.192.0.0]创建一个列表;''对于每个错误,是否可以这样做?进入一个数据数组并通过它循环 –
你会得到我想要做的吗?也许我会错误的方式吗? –