Nach einer langen Diskussion im Chat, schlage ich den folgenden Ansatz für Unit-Tests vor, sowie eine leichte Umstrukturierung des eigentlichen Codes, um die Dinge ein wenig einfacher zu machen.
Änderungen, die ich
ich die Art und Weise umstrukturiert der Code die Fehlermeldung aus einer Vorlage erstellt nicht Template verwenden, da es von your previous question klar war, dass es ein bisschen übertrieben ist.
Es verwendet jetzt sprintf
mit einem einfachen Muster wie Timeout after %s seconds
. Ich habe %s
in meinen Beispielen absichtlich verwendet, da es in diesen keine Typüberprüfung gibt, aber es könnte natürlich hinzugefügt werden. Die Argumente für diese Nachricht werden dem Konstruktor als eine Liste von Schlüssel/Wert-Paaren übergeben, die mit dem 2. Argument beginnen.
my $e = Error->new(CONSTANT, foo => 'bar');
Das Beispiel ErrorLibrary
Das erste Argument CONSTANT
noch aus Ihrer Fehler Bibliothek kommt. Ich habe die folgenden vereinfachten Beispiele aufgenommen.
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);
Der Kontext ist eine Array-Referenz mit einer Liste von Schlüsseln, die bei Konstruktion zu erwarten sind.
Die umstrukturierte (vereinfacht) Fehlerklasse
Diese Klasse umfasst POD zu erklären, was es tut. Die wichtigsten Methoden sind der Konstruktor message
und 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
Die tatsächliche Einheit testet
Nun dies zu testen, ist es wichtig, zwischen Verhalten und Daten zu unterscheiden. Das Verhalten enthält alle Accessoren, die im Code definiert sind, sowie die interessanteren Subs wie new
, message
und stringify
.
Der erste Teil der Testdatei, die ich für dieses Beispiel erstellt habe, enthält diese.Es schafft eine gefälschte Fehlerstruktur $example_error
und verwendet es, um zu überprüfen, dass der Konstruktor mit den richtigen Parametern umgehen können, oder überschüssige Parameter fehlen, dass die Accessoren das Zeug zurückzukehren, und dass message
und stringify
beide schaffen den richtigen Inhalt.
Denken Sie daran, dass diese Tests hauptsächlich ein Sicherheitsnetz, wenn eine Änderung des Codes (vor allem nach ein paar Monaten). Wenn Sie versehentlich etwas an der falschen Stelle ändern, werden die Tests fehlschlagen.
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}
);
};
Es fehlen einige spezifische Testfälle. Wenn Sie ein metrisches Tool wie Devel::Cover verwenden, ist es erwähnenswert, dass eine vollständige Abdeckung nicht bedeutet, dass alle möglichen Fälle abgedeckt sind.
Tests für Ihre Fehlerdatenqualität
nun den zweiten Teil, der in diesem Beispiel wert ist bedeckt, ist die Richtigkeit der Fehlervorlagen in ErrorLibrary. Jemand könnte versehentlich später etwas verwechseln, oder es könnte ein neuer Platzhalter zu einer Nachricht hinzugefügt werden, aber nicht zu dem Kontext-Array.
Der folgende Testcode wird idealerweise in eine eigene Datei eingefügt und nur ausgeführt, wenn Sie mit der Bearbeitung eines Features fertig sind. Zur Veranschaulichung wird dies jedoch nur nach dem obigen Codeblock fortgesetzt, daher die beiden ersten Ebenen subtest
s .
Der Hauptteil Ihrer Frage war über die Liste der Testfälle. Ich halte das für sehr wichtig. Sie möchten, dass Ihr Testcode sauber, leicht zu lesen und noch einfacher zu warten ist. Der Test ist häufig eine Dokumentation, und nichts ist ärgerlicher, als den Code zu ändern und dann herauszufinden, wie die Tests funktionieren, damit Sie sie aktualisieren können. Also immer daran denken:
Tests sind auch Produktionscode!
Schauen wir uns nun die Tests für die Fehler an.
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;
Es hat im Grunde eine Reihe von Testfällen, mit einem Fall für jede der möglichen Fehlerkonstanten, die von ErrorLibrary exportieren bekommen. Es hat den Namen, der verwendet wird, um den richtigen Fehler zu laden und den Testfall in der TAP-Ausgabe zu identifizieren, die erforderlichen Argumente zum Ausführen des Tests und die erwartete endgültige Ausgabe. Ich habe nur Nachricht aufgenommen, um es kurz zu halten.
Wenn ein Fehlervorlagenname in ErrorLibrary geändert (oder entfernt) wird, ohne den Text zu ändern, schlägt die lives_ok
Umgebung der Objekt Instanziierung fehl, weil dieser Name nicht exportiert wurde. Das ist ein schönes Plus.
Es wird jedoch nicht fangen, wenn ein neuer Fehler ohne einen Testfall hinzugefügt wurde. Ein Ansatz dafür wäre, die Symboltabelle im main
-Namespace zu betrachten, aber das ist für den Umfang dieser Antwort ein wenig zu weit fortgeschritten.
Was es auch tut, ist LWP::Simple verwenden, um eine HEAD
HTTP-Anfrage an jede Wiki-URL zu tun, um zu sehen, ob diese erreichbar sind. Das hat auch den Vorteil, dass es beim Ausführen eines Builds ein wenig wie ein Monitoring-Tool funktioniert.
es bringt alle zusammen
Schließlich ist hier der TAP-Ausgang, wenn sie ohne prove
lief.
# 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
Bitte klären Sie, was das Problem ist. Fragen Sie, wie Sie den Komponententest strukturieren, um Code-Duplikation zu vermeiden? Du sprichst von "Prozess", was von deinen vorherigen Fragen, glaube ich, eine Methode ist. Warum sollte das in den Komponententest gehen? – simbabque
Nein, ich möchte nur wissen, wie ich meinen Test einrichten kann, so dass ich, anstatt einen Fehler testen zu können, eine for-Schleife oder ähnliches verwenden kann, um die Tests für mehrere Fehler auszuführen. Ich wollte den Wortprozess nicht hineinbringen. Ich dachte nur, dass ich eine Liste mit 'data = erstellen sollte [input => UNABLE_TO_PING_SWITCH_ERROR, Ausgabe =>" Switch 192.192.0.0 konnte in 30 Sekunden nicht gepingt werden]; 'Wäre es möglich, so etwas für jeden Fehler zu tun, setzen in eine Anordnung von Daten und Schleife durch sie –
bekommen Sie, was ich versuche ich werde den falschen Weg, um es zu tun? Vielleicht? –