Las Pruebas

Testing versus Debugging

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.

Los Objetivos de las Pruebas

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.

Que Comprobar

La Preparación de Las Pruebas

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.dat
Ademá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.

Comprobar que el Módulo se Carga

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/); }

Establecer un Correspondencia Entrada/Salida Correcta

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:

Uso de Data::Dumper y del Depurador para la Preparación de las Pruebas

En el proceso de elaboración de una prueba para una subrutina es necesario tener una descripción Perl de

Esta descripción puede obtenerse usando Data::Dumper o el depurador o ambas cosas.
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];

Las Funciones is_deeply e is

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.

Como Evitar la Muerte (de un Programa)

¿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.

El Módulo Test::Exception

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';
}

Pruebas SKIP

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).

Pruebas TODO

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') failed
Obsérvese que: Sigue una ejecución con 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.

Devel::Cover: Estudio del Cubrimiento de las Pruebas

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.

Estudio del Perfil de Rendimiento

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.000s
La 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.out
Al 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



Subsecciones
Casiano Rodríguez León
2009-10-04