經過長時間的聊天討論,我提出了以下方法來進行單元測試,以及輕鬆重構實際代碼以使事情變得更容易。
變化我做
我已經重構的代碼從模板創建錯誤消息不使用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]創建一個列表;''對於每個錯誤,是否可以這樣做?進入一個數據數組並通過它循環 –
你會得到我想要做的嗎?也許我會錯誤的方式嗎? –