El tiempo invertido en Comprobar (testing) y el tiempo invertido en Depurar (debugging) mantiene una relación inversa. Cuanto menor es el tiempo invertido en la preparación de pruebas mayor será nuestro tiempo de depuración. Cuanto mejor sea nuestra suite de pruebas menor será el tiempo que necesitemos para fijar errores en nuestros programas.
Las pruebas no aseguran la corrección de nuestro programa. Las pruebas descubren errores. Una prueba tiene éxito cuando falla y demuestra la presencia de un error.
En palabras de Bruce Eckel (Thinking in Java):
You can write all the prose, or create all the diagrams you want, describing how a class should behave and what it looks like, but nothing is as real as a set of tests. The former is a wish list, but the tests are a contract that is enforced by the compiler and the running program. It's hard to imagine a more concrete description of a class than the tests
Cuando escriba las pruebas invierta su escala de valores: Alégrese cuando una prueba descubre un error. Es peor que el error esté ahí y no sea descubierto.
'0'
, '0E0'
, listas vacías, hashes vacíos, etc.
Ahora estudiemos el programa de prueba. Está en el directorio t
.
$ pwd /home/lhp/Lperl/src/threads/knapsack/Algorithm-Knap01DP/t $ ls -l -rw-r--r-- 1 lhp lhp 1693 2005-05-17 19:57 01MartelloAndTothBook.t -rw-r--r-- 1 lhp lhp 134 2005-05-16 18:37 knap21.dat -rw-r--r-- 1 lhp lhp 125 2005-05-16 18:37 knap22.dat -rw-r--r-- 1 lhp lhp 123 2005-05-16 18:37 knap23.dat -rw-r--r-- 1 lhp lhp 158 2005-05-16 18:37 knap25.datAdemás del programa
01MartelloAndTothBook.t
tenemos cuatro ficheros
con cuatro diferentes problemas de la mochila. Los números corresponden
a las páginas del clásico libro de Martello y Toth [15]
en el que aparece el correspondiente
problema.
Puede encontrar el texto completo del programa 01MartelloAndTothBook.t
en la sección
10.1.
Realizaremos 11 pruebas de las que la primera es comprobar que el módulo se carga correctamente (línea 8).
$ cat -n 01MartelloAndTothBook.t 1 # Before `make install' is performed this script should be runnable with 2 # `make test'. After `make install' it should work as `perl Algorithm-Knap01DP.t' 3 4 ######################### 5 use strict; 6 use Test::More tests => 11; 7 8 BEGIN { use_ok('Algorithm::Knap01DP', qw/Knap01DP ReadKnap/); }
Para el establecimiento de las pruebas para una subrutina dada es necesario definir las estructuras de datos para la llamada y las correspondientes que resultan de una correcta ejecución de dicha subrutina:
10 ### main 11 my @inputfiles = qw/knap21.dat knap22.dat knap23.dat knap25.dat/; 12 my @sol = (280, 107, 150, 900); 13 my $knap21 = ['102', [ '2', '20', '20', '30', '40', '30', '60', '10' ], 14 [ '15', '100', '90', '60', '40', '15', '10', '1' ]]; 15 my $knap22 = ['50', [ '31', '10', '20', '19', '4', '3', '6' ], 16 [ '70', '20', '39', '37', '7', '5', '10' ]]; 17 my $knap23 = ['190', [ '56', '59', '80', '64', '75', '17' ], 18 [ '50', '50', '64', '46', '50', '5' ]]; 19 my $knap25 = ['104', [ '25', '35', '45', '5', '25', '3', '2', '2' ], 20 [ '350', '400', '450', '20', '70', '8', '5', '5' ]]; 21 22 my $knapsackproblem = [$knap21, $knap22, $knap23, $knap25];
En nuestro caso esa definición de la correspondencia entre una entrada y su correspondiente salida correcta se traduce en:
@inputfiles
contiene los nombres de los ficheros
de prueba.
$knap21
...$knap25
contienen
estructuras de datos que definen los problemas: capacidad de la mochila, vector de pesos
y vector de beneficios.
@sol
las soluciones óptimas a esos
problemas.
En el proceso de elaboración de una prueba para una subrutina es necesario tener una descripción Perl de
pp2@nereida:/tmp/Algorithm-Knap01DP-0.01/t$ perl -I../lib "-MAlgorithm::Knap01DP=Knap01DP,ReadKnap" -wde 0 main::(-e:1): 0 DB<1> $x = ReadKnap("knap22.dat") DB<2> use Data::Dumper DB<3> Dumper($x) DB<4> p Dumper($x) $VAR1 = [ '70', '20', '39', '37', '7', '5', '10' ]; DB<5> x $x 0 ARRAY(0x840a11c) 0 70 1 20 2 39 3 37 4 7 5 5 6 10
Cuando la función ReadKnap
lee un fichero
de datos devuelve una estructura como la descrita.
De hecho, es usando el depurador, cortando y pegando que hemos construido
parte del código de pruebas
definiendo las estructuras de datos $knapXX
:
10 ### main 11 my @inputfiles = qw/knap21.dat knap22.dat knap23.dat knap25.dat/; 12 my @sol = (280, 107, 150, 900); 13 my $knap21 = ['102', [ '2', '20', '20', '30', '40', '30', '60', '10' ], 14 [ '15', '100', '90', '60', '40', '15', '10', '1' ]]; 15 my $knap22 = ['50', [ '31', '10', '20', '19', '4', '3', '6' ], 16 [ '70', '20', '39', '37', '7', '5', '10' ]]; 17 my $knap23 = ['190', [ '56', '59', '80', '64', '75', '17' ], 18 [ '50', '50', '64', '46', '50', '5' ]]; 19 my $knap25 = ['104', [ '25', '35', '45', '5', '25', '3', '2', '2' ], 20 [ '350', '400', '450', '20', '70', '8', '5', '5' ]]; 21 22 my $knapsackproblem = [$knap21, $knap22, $knap23, $knap25];
A continuación leeemos cada fichero y comprobamos que ambas ReadKnap
y Knap01DP
dan los resultados esperados.
La función
is_deeply nos dice si dos estructuras de datos son equivalentes.
Véase perldoc Test::More
para mas información sobre
el módulo Test::More y las funciones is_deeply
e is.
24 my $i = 0; 25 my ($M, $w, $p); 26 my @f; 27 28 # Now 2*@inputfiles = 8 tests 29 for my $file (@inputfiles) { 30 ($M, $w, $p) = ReadKnap((-e "t/$file")?"t/$file":$file); 31 is_deeply($knapsackproblem->[$i], [$M, $w, $p], "ReadKnap $file"); 32 my $N = @$w; 33 @f = Knap01DP($M, $w, $p); 34 is($sol[$i++], $f[$N-1][$M], "Knap01DP $file"); 35 }
Para mas funciones de Comparación Profunda véase el módulo Test::Deep.
¿Cual es el comportamiento correcto de una función cuando es llamada con
argumentos erróneos?
Parece razonable que el comportamiento de tal función sea advertir
al programador de la conducta anómala y en su caso detener la ejecución
del programa llamando a die
o a croak
.
¿Que ocurre si dentro de un programa de prueba .t
queremos comprobar el funcionamiento de tal subrutina bajo
situaciones de ''stress'' como esta, por ejemplo,
con argumentos erróneos?
La respuesta es que - si se hace una llamada convencional - la llamada
a die
dentro de la subrutina investigada provocará la ''muerte prematura''
del programa de pruebas.
A continuación realizamos una prueba para comprobar el funcionamiento
cuando se le pasan a Knap01DP
vectores de pesos y beneficios de
distinto tamaño. Recordemos que en la rutina Knap01DP
habíamos
escrito el siguiente código:
14 sub Knap01DP { 15 my $M = shift; 16 my @w = @{shift()}; 17 my @p = @{shift()}; 18 19 my $N = @w; 20 my @f; 21 22 croak "Profits and Weights don't have the same size" unless scalar(@w) == scalar(@p); 23 .. .................................................... 38 return @f; 39 } 40 41 sub ReadKnap { 42 my $filename = shift; 43 44 my $file = IO::File->new("< $filename"); 45 croak "Can't open $filename" unless defined($file); .. ............ 47 55 return ($M, \@w, \@p); 56 }por tanto, pasarle a la rutina vectores de distinto tamaño hace que el programa muera. Es por esto que protegeremos la ejecución dentro de un
eval
:
37 # test to check when weights and profits do not have the same size 38 $M = 100; @$w = 1..5; @$p = 1..10; 39 eval { Knap01DP($M, $w, $p) }; 40 like $@, qr/Profits and Weights don't have the same size/;
Ahora la llamada a croak
dentro de Knap01DP
sólo produce
la finalización del eval. El mensaje emitido por croak
o die
queda en la variable especial $@
.
La función like comprueba que el primer argumento casa con la expresión regular especificada en el segundo argumento.
Una alternativa es usar el módulo Test::Exception si está instalado:
BEGIN { $test_exception_installed = 1; eval { require Test::Exception }; $test_exception_installed = 0 if $@; }para posteriormente aplicar la prueba:
SKIP: { skip "Test::Exception not installed", 1 unless $test_exception_installed; Test::Exception::lives_ok { Parse::Eyapp->new_grammar( input=>$translationscheme) } 'No errors in input translation scheme'; }
El código anterior muestra una prueba SKIP. Una prueba SKIP declara un bloque de pruebas que - bajo ciertas circustancias - puede saltarse. Puede ser que sepamos que ciertas pruebas sólo funcionan en ciertos sistemas operativos o que la prueba requiera que ciertos paquetes están instalados o que la máquina disponga de ciertos recursos (por ejemplo, acceso a internet).
Vamos a hacer una prueba mas. Supongamos que tengo la intención de
añadir una función GenKnap
que genere aleatoriamente un problema de
la mochila. Como no esta hecho,
lo declaramos como una prueba a hacer (TODO). Es decir, se trata de un
test que fallará, pero que se espera que deje de hacerlo en el futuro.
42 TODO: { 43 local $TODO = "Randomly generated problem"; 44 can_ok('Algorithm::Knap01DP', 'GenKnap'); 45 }Primero una ejecución a mano:
~/Lperl/src/threads/knapsack/Algorithm-Knap01DP/t$ perl \ -I/home/lhp//Lperl/src/threads/knapsack/Algorithm-Knap01DP/lib 01MartelloAndTothBook.t 1..11 ok 1 - use Algorithm::Knap01DP qw/Knap01DP ReadKnap/;; ok 2 - ReadKnap knap21.dat ok 3 - Knap01DP knap21.dat ok 4 - ReadKnap knap22.dat ok 5 - Knap01DP knap22.dat ok 6 - ReadKnap knap23.dat ok 7 - Knap01DP knap23.dat ok 8 - ReadKnap knap25.dat ok 9 - Knap01DP knap25.dat ok 10 not ok 11 - Algorithm::Knap01DP->can('GenKnap') # TODO Randomly generated problem # Failed (TODO) test (01MartelloAndTothBook.t at line 45) # Algorithm::Knap01DP->can('GenKnap') failedObsérvese que:
t
.
-I
para que pueda encontrar el módulo.
not ok 11 - ... # TODO Return ...
indicando que falla y que es una prueba TODO
.
make test
(un directorio por encima):
~/Lperl/src/threads/knapsack/Algorithm-Knap01DP$ make test PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" \ "-e" "test_harness(0, 'blib/lib', 'blib/arch')" \ t/*.t t/01MartelloAndTothBook....ok All tests successful. Files=1, Tests=11, 0 wallclock secs ( 0.09 cusr + 0.00 csys = 0.09 CPU)Observa como ahora se informa que todas las pruebas fueron correctamente. Se ha ocultado que hay una prueba
TODO
y su fallo no se considera significativo para
la posible instalación del módulo. De este modo el directorio
de pruebas puede ser utilizado como lista recordatorio de objetivos
y requerimientos a realizar.
Instale el módulo Devel::Cover. El módulo Devel::Cover ha sido escrito por Paul Johnson y proporciona estadísticas del cubrimiento alcanzado por una ejecución. Para usarlo siga estos pasos:
hp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP$ cover -delete Deleting database /home/lhp/projects/perl/src/threads/knapsack/Algorithm-Knap01DP/cover_db lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP$ make HARNESS_PERL_SWITCHES=-MDevel::Cover test PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t t/01alltests....ok t/02bench.......ok All tests successful. Files=2, Tests=16, 18 wallclock secs (17.05 cusr + 0.75 csys = 17.80 CPU) Writing HTML output to /home/lhp/projects/perl/src/threads/knapsack/Algorithm-Knap01DP/cover_db/coverage.html ... done.La ejecución toma ahora mas tiempo. Al ejecutar
cover
de nuevo obtenemos una tabla con las estadísticas
de cubrimiento:
lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP$ cover Reading database from /home/lhp/projects/perl/src/threads/knapsack/Algorithm-Knap01DP/cover_db ---------------------------- ------ ------ ------ ------ ------ ------ ------ File stmt bran cond sub pod time total ---------------------------- ------ ------ ------ ------ ------ ------ ------ ...lib/Algorithm/Knap01DP.pm 100.0 82.1 37.5 100.0 0.0 100.0 88.8 Total 100.0 82.1 37.5 100.0 0.0 100.0 88.8 ---------------------------- ------ ------ ------ ------ ------ ------ ------ Writing HTML output to /home/lhp/projects/perl/src/threads/knapsack/Algorithm-Knap01DP/cover_db/coverage.html ... done.El HTML generado nos permite tener una visión mas detallada de los niveles de cubrimiento.
lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP$ cd cover_db/ lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP/cover_db$ ls -l total 68 -rw-r--r-- 1 lhp lhp 3424 2007-05-16 15:48 blib-lib-Algorithm-Knap01DP-pm--branch.html -rw-r--r-- 1 lhp lhp 3127 2007-05-16 15:48 blib-lib-Algorithm-Knap01DP-pm--condition.html -rw-r--r-- 1 lhp lhp 34909 2007-05-16 15:48 blib-lib-Algorithm-Knap01DP-pm.html -rw-r--r-- 1 lhp lhp 2207 2007-05-16 15:48 blib-lib-Algorithm-Knap01DP-pm--subroutine.html -rw-r--r-- 1 lhp lhp 3345 2007-05-16 15:48 cover.12 -rw-r--r-- 1 lhp lhp 2004 2007-05-16 15:48 coverage.html -rw-r--r-- 1 lhp lhp 1348 2007-05-16 15:48 cover.css drwx------ 2 lhp lhp 4096 2007-05-16 15:48 runs drwx------ 2 lhp lhp 4096 2007-05-16 15:48 structure
Para mejorar el cubrimiento de tu código comienza por el informe de cubrimiento de subrutinas. Cualquier subrutina marcada como no probada es un candidato a contener errores o incluso a ser código muerto.
Se conoce con el nombre de perfilado o profiling de un programa al estudio de su rendimiento mediante un programa (conocido como profiler) que monitoriza la ejecución del mismo mediante una técnica que interrumpe cada cierto tiempo el programa para comprobar en que punto de la ejecución se encuentra. Las estadísticas acumuladas se vuelcan al final de la ejecución en un fichero que puede ser visualizado mediante la aplicación apropiada.
En Perl hay varios módulos que permiten realizar profiling. El mas antiguo es Devel::DProf. La aplicación para visualizar los resultados se llama dprofpp. Sigue un ejemplo de uso:
lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP/t$ time perl -MDevel::Profiler usealknap.pl knap25.dat 14,5,11 14,5,3,8 14,2,11,3 real 0m0.065s user 0m0.060s sys 0m0.000sLa ejecución crea un fichero
tmon.out
:
lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP/t$ ls -ltr | tail -1 -rw-r--r-- 1 lhp lhp 9876 2007-05-16 16:10 tmon.outAl ejecutar dprofpp se muestran las subrutinas ordenadas según su consumo de CPU:
lhp@nereida:~/Lperl/src/threads/knapsack/Algorithm-Knap01DP/t$ dprofpp tmon.out Total Elapsed Time = 0.002055 Seconds User+System Time = 5.5e-05 Seconds Exclusive Times %Time ExclSec CumulS #Calls sec/call Csec/c Name 0.00 0.000 0.000 52 0.0000 0.0000 Algorithm::Knapsack::_knapsack 0.00 0.000 0.000 1 0.0000 0.0001 Algorithm::Knapsack::compute 0.00 0.000 0.000 1 0.0000 0.0000 Algorithm::Knapsack::solutions 0.00 0.000 0.000 1 0.0000 0.0000 Algorithm::Knapsack::new