El módulo Test::LectroTest

Generación de Pruebas a Partir de la Especificación de Propiedades

El módulo Test::LectroTest por Tom Moertel permite la generación de tests mediante la especificación de propiedades. Así en vez de escribir algo como:

use Test::More tests => 4;
is( sqrt(0), 0, "sqrt(0) == 0" );
is( sqrt(1), 1, "sqrt(1) == 1" );
is( sqrt(4), 2, "sqrt(4) == 2" );
is( sqrt(9), 3, "sqrt(9) == 3" );
podemos hacer:
nereida:~/projects/coro/xpp/check> cat -n electrotest.pl
 1  #!/usr/bin/perl -w
 2  use Test::LectroTest;
 3  Property {
 4     ##[ x <- Float(range=>[0,1_000]) ]##
 5     sqrt( $x * $x ) == $x;
 6  }, name => "sqrt satisfies defn of square root";
 7
 8  Property {
 9     ##[ x <- Int ]##
10     $x + $x  >= $x;
11  }, name => "2*x is greater than x";
nereida:~/projects/coro/xpp/check> ./electrotest.pl
1..2
ok 1 - 'sqrt satisfies defn of square root' (1000 attempts)
not ok 2 - '2*x is greater than x' falsified in 3 attempts
# Counterexample:
# $x = -3;

Uso de Test::LectroTest Dentro de Test::More

Para el uso del módulo con Test::More utilice el módulo Test::LectroTest::Compat. Este módulo permite mezclar la comprobación mediante propiedades con la familia de los módulos Test::. Es posible llamar a las funciones is() y ok() dentro del código de especificación de una propiedad. Véase el uso de cmp_ok en la línea 10 del siguiente ejemplo:

lhp@nereida:~/Lperl/src/LectroTest/Titi$ cat -n t/01lectro.t
 1  use Titi;  # contains code we want to test
 2  use Test::More tests => 2;
 3  use Test::LectroTest::Compat;
 4
 5  # property specs can now use Test::Builder-based
 6  # tests such as Test::More's cmp_ok()
 7
 8  my $prop_nonnegative = Property {
 9    ##[ x <- Int, y <- Int ]##
10    cmp_ok(Titi::my_function( $x, $y ), '>=', 0);
11  }, name => "my_function output is non-negative" ;
12
13  # and we can now check whether properties hold
14  # as Test::Builder-style tests that integrate
15  # with other T::B tests
16
17  holds( $prop_nonnegative ); # test whether prop holds
18  cmp_ok( 0, '<', 1, "trivial 0<1 test" ); # a "normal" test
Como se ve en la línea 17 se provee también una función holds la cual somete a pruebas la propiedad que recibe como argumento.

La Función holds

La función holds:

       holds(property, opts...)

       holds( $prop_nonnegative );  # check prop_nonnegative

       holds( $prop_nonnegative, trials => 100 );

       holds(
          Property {
             ##[ x <- Int ]##
             my_function2($x) < 0;
          }, name => "my_function2 is non-positive"
       );

Comprueba el cumplimiento de la propiedad. Cuando se la llama crea un objeto Test::LectroTest::TestRunner al que solicita la comprobación de la propiedad, informando del resultado a Test::Builder el cual a su vez informa a Test::Simple o Test::More (función plan). Las opciones que se provean a holds serán pasadas al objeto TestRunner de manera que se puede cambiar el número de pruebas, etc. (véase la documentación de Test::LectroTest::TestRunner para la lista completa de opciones).

El Módulo a Comprobar

La prueba t/01lectro.t la usamos para comprobar el siguiente módulo Titi.pm. Esta es la jerarquía de directorios:

lhp@nereida:~/Lperl/src/LectroTest$ tree Titi/
Titi/
|-- Changes
|-- MANIFEST
|-- Makefile.PL
|-- README
|-- lib
|   `-- Titi.pm
`-- t
    |-- 01lectro.t
        `-- Titi.t

        2 directories, 7 files
En concreto la prueba t/01lectro.t vista antes comprueba que la función my_function abajo devuelve valores positivos:
lhp@nereida:~/Lperl/src/LectroTest/Titi$ cat -n lib/Titi.pm
 1  package Titi;
 2
 3  use 5.008007;
 4  use strict;
 5  use warnings;
 6
 7  require Exporter;
 8
 9  our @ISA = qw(Exporter);
10  our @EXPORT = qw( my_function );
11  our $VERSION = '0.01';
12
13  # Preloaded methods go here.
14  sub my_function {
15    my ($x, $y) = @_;
16    $x*$y*$y
17  }
18
19  1;

Ejecución

Cuando ejecutamos las pruebas obtenemos un contraejemplo a nuestra aserción de que my_function devuelve valores positivos:

lhp@nereida:~/Lperl/src/LectroTest/Titi$ make test
PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" 
         "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/01lectro....
t/01lectro....NOK 1#     Failed test (t/01lectro.t at line 17)
#     '-9'
#         >=
#     '0'
#     Counterexample:
#     $x = -1;
#     $y = 3;
# Looks like you failed 1 test of 2.
t/01lectro....dubious
        Test returned status 1 (wstat 256, 0x100)
DIED. FAILED test 1
        Failed 1/2 tests, 50.00% okay
t/Titi........ok
Failed Test  Stat Wstat Total Fail  Failed  List of Failed
-------------------------------------------------------------------------------
t/01lectro.t    1   256     2    1  50.00%  1
Failed 1/2 test scripts, 50.00% okay. 1/3 subtests failed, 66.67% okay.
make: *** [test_dynamic] Error 255

Un Algebra de Generadores

El módulo Test::LectroTest::Generator provee generadores para los tipos de datos mas comunes. Provee además un algebra de combinadores de generadores que permite la construcción de generadores complejos.

$ perl -wde 0
  DB<1> use Test::LectroTest::Generator qw(:common Gen)
  DB<2> p 1<<31
2147483648
  DB<3> $tg = Int(range=>[0, 2_147_483_647], sized=>0)
  DB<4> x $tg
0  Test::LectroTest::Generator=HASH(0x84faa78)
   'generator' => CODE(0x84fa9a0)
      -> &Test::LectroTest::Generator::\
      __ANON__[/usr/local/share/perl/5.8.7/Test/LectroTest/Generator.pm:212]\
      in /usr/local/share/perl/5.8.7/Test/LectroTest/Generator.pm:210-212
Una expresión como Int(range=>[0, 2_147_483_647], sized=>0) construye un generador aleatorio de enteros.

Si el argumento sized fuera cierto (que es el valor por defecto) los enteros generados lo serían de acuerdo con una cierta 'política de tamaños'. El generador es un objeto que contiene un método generate. Aqui el argumento sized puesto a falso indica que los valores generados sólo están restringidos por el rango especificado.

  DB<3> x $tg->generate
0  1760077938
  DB<4> x scalar localtime $tg->generate
0  'Wed Jan 14 00:07:07 1998'

La subrutina Gen permite la construcción de nuevos generadores:

  DB<7> $ctg = Gen { scalar localtime $tg->generate( @_ ) }
  DB<8> print $ctg->generate()."\n" for 1..3
Thu Aug 15 15:34:05 2019
Sat Sep  7 04:11:15 1974
Sun Jun 19 14:30:14 1977
de esta forma hemos construido un generador aleatorio de fechas.

Existen múltiples generadores básicos y un conjunto de combinadores de generadores. Sigue un ejemplo que muestra la influencia del parámetro sizing guidance:

  DB<1> use Test::LectroTest::Generator qw(:common :combinators)
  DB<2> $int_gen = Int
  DB<3> print $int_gen->generate($_)." " for 1..100 # El arg es sizing guidance
1 1 0 0 -2 3 -3 8 -6 6 6 6 -9 -10 2 1 -9 -11 4 12 19 -11 16 -1 -22 12 -13 -20 -12 9 \
-21 -9 21 -27 5 -19 -20 28 -18 -31 -14 2 -40 -10 33 32 1 33 6 21 3 5 45 31 -28 20 -17 \
-9 58 48 -58 2 -40 27 6 -20 -41 30 59 40 -49 -60 -38 44 -44 -63 12 -76 45 41 65 \
63 -20 59 -5 62 0 65 63 34 71 32 59 -61 -7 -14 30 -71 -13 -58

Veamos algunos constructores de generadores básicos:

  DB<4> $flt_gen = Float( range=>[0,1] )
  DB<5> x  $flt_gen->generate
0  0.793049252432869
  DB<6> x  $flt_gen->generate
0  0.543474544861482
  DB<7> $bln_gen = Bool
  DB<8> x $bln_gen->generate
0  0
  DB<9> x $bln_gen->generate
0  1
  DB<10> x $bln_gen->generate
0  1
  DB<11> $chr_gen = Char( charset=>"a-z" )
  DB<12> x $chr_gen->generate
0  's'
  DB<13> x $chr_gen->generate
0  'u'
  DB<14> x $chr_gen->generate
0  'i'
  DB<15> $elm_gen = Elements("e1", "e2", "e3", "e4")
  DB<16> x $elm_gen->generate
0  'e2'
  DB<17> x $elm_gen->generate
0  'e4'
  DB<18> x $elm_gen->generate
0  'e2'

El combinador Frequency permite construir un generador a partir de una lista de generadores que produce los elementos con probabilidades proporcionales a las frecuencias asignadas:

  DB<19> $english_dist_vowel_gen = Frequency([8.167,Unit("a")], [12.702,Unit("e")], \
                          [6.996,Unit("i")], [ 7.507,Unit("o")],[2.758,Unit("u")] )
  DB<20> x $english_dist_vowel_gen->generate
0  'i'
  DB<21> x $english_dist_vowel_gen->generate
0  'e'
  DB<22> x $english_dist_vowel_gen->generate
0  'i'
0  'o'

Otro combinador es Paste:

  DB<23> $digit_gen  = Elements( 0..9 )
  DB<24> $ssn_gen = Paste(Paste(($digit_gen)x3),Paste(($digit_gen)x2),Paste(($digit_gen)x4),glue => "-")
  DB<25> x $ssn_gen->generate
0  '168-19-1333'
  DB<26> x $ssn_gen->generate
0  '348-35-8320'

Veamos el generador String:

  DB<27>  $gen = String( length=>[3,5], charset=>"A-Z", size => 100 )
  DB<28> x $gen->generate
0  'KBZNB'
  DB<29> x $gen->generate
0  'AAK'
  DB<30> x $gen->generate
0  'AZL'

Un ejemplo con List:

  DB<31> $ary_gen = List( Int(sized=>0), length => 5 )
  DB<32> x $ary_gen->generate
0  ARRAY(0x8563850)
   0  19089
   1  '-13489'
   2  10390
   3  5382
   4  981
  DB<33> x $ary_gen->generate
0  ARRAY(0x853f030)
   0  17062
   1  18558
   2  29329
   3  31931
   4  2464

También podemos construir generadores de hashes:

  DB<34> $gen = Hash( String( charset=>"A-Z", length=>3 ), Float( range=>[0.0, 100.0] ), length => 4)
  DB<35> x $gen->generate
0  HASH(0x8576a30)
   'FSW' => 0.998256374071719
   'KLR' => 0.0577333231717212
   'PEV' => 0.834037952293134
   'TNK' => 0.0146371360307889

El combinador OneOf aplica uno de varios generadores:

  DB<36> $gen = OneOf( Unit(0), List(Int,length=>3) )
  DB<37> x $gen->generate
0  ARRAY(0x856c454)
   0  '-1'
   1  0
   2  0
  DB<38> x $gen->generate
0  0

El combinador Each permite generar listas formados de aplicar cada uno de los generadores:

  DB<39> $gen = Each( Char(charset => "aeiou"), Int( range=>[0,10], sized => 0 ) )
  DB<40> x $gen->generate
0  ARRAY(0x857b770)
   0  'o'
   1  3
  DB<41> x $gen->generate
0  ARRAY(0x85771bc)
   0  'e'
   1  5
  DB<42> x $gen->generate
0  ARRAY(0x857b080)
   0  'i'
   1  3

El combinador Apply aplica el código dado al resultado producido opr los generadores especificados:

  DB<42> $gen = Apply( sub { $_[0] x $_[1] }, Char( charset=>'a-z'), Unit(4) )
  DB<43> x $gen->generate
0  'xxxx'
  DB<44> x $gen->generate
0  'hhhh'
  DB<45> x $gen->generate
0  'jjjj'

El siguiente ejemplo produce matrices 3x3:

  DB<46> $loloi_gen = List( List( Int(sized=>0), length => 3 ), length => 3)
  DB<47> x $loloi_gen->generate
0  ARRAY(0x857bd1c)
   0  ARRAY(0x8576a90)
      0  9648
      1  2796
      2  9589
   1  ARRAY(0x8576dcc)
      0  '-29523'
      1  '-21714'
      2  31931
   2  ARRAY(0x857658c)
      0  '-9477'
      1  '-2434'
      2  '-3794'

Ejercicio 5.21.1   Construya un generador que produzca problemas de la mochila 0-1 representados mediante una estructura de datos de la forma
[$Capacity, [$w_0, ..., $w_n], [$p_0, ..., $p_n]]

Usando Test::LectroTest::Generator en Algorithm::Knap01DP

El módulo Test::LectroTest::Generator puede ser usado para generar entradas cuya solución sea conocida y de esta forma comprobar el buen funcionamiento del algoritmo.

En el ejemplo que sigue (fichero t/03lectro.t) se generan problemas de 10 objetos con los beneficios (profits) iguales a los pesos. Después se genera un conjunto solución (líneas 15-19). Al elegir la capacidad de la mochila igual a la suma de los pesos del subconjunto generado podemos estar seguros que dicho subconjunto es una solución y que el valor óptimo es igual a la capacidad. De hecho este subproblema de la mochila es conocido como problema del subconjunto suma (Subset Sum Problem o SSP).

lhp@nereida:~/Lperl/src/perl_testing_adn_examples/chapter_03/Algorithm-Knap01DP-0.25$ cat -n t/03lectro.t
 1  use strict;
 2  use Test::More;
 3  use Test::LectroTest::Generator qw(:common);
 4  use List::Util qw(sum);
 5
 6  use Algorithm::Knap01DP;
 7
 8  my $t = shift || 100;
 9  plan tests => $t;
10  my ($C, @sol);
11  for (1..$t) {
12    my $weights = List(Int(range => [5,100], sized=>0), length => 10);
13    my @w = @{$weights->generate};
14    my @p = @w;
15    my $set = List(Bool, length => 10);
16    do {
17      @sol = @{$set->generate};
18      $C = sum( @w[ grep { $sol[$_] } 0..9 ] );
19    } while ($C == 0);
20    my $knap = Algorithm::Knap01DP->new( capacity => $C, weights => \@w, profits => \@p);
21    $knap->Knap01DP();
22    is($C, $knap->{tableval}[-1][-1], "Random subset-sum problem");
23  }
Observe como el uso de plan en la línea 9 nos permite ajustar dinámicamente el número de pruebas a ejecutar. Podemos hacer una ejecución vía make:
lhp@nereida:~/Lperl/src/perl_testing_adn_examples/chapter_03/Algorithm-Knap01DP-0.25$ make 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
t/03lectro......ok
All tests successful.
Files=3, Tests=116,  4 wallclock secs ( 4.08 cusr +  0.02 csys =  4.10 CPU)
o una ejecución ''manual'':
lhp@nereida:~/Lperl/src/perl_testing_adn_examples/chapter_03/Algorithm-Knap01DP-0.25/t$ perl 03lectro.t 4
1..4
ok 1 - Random subset-sum problem
ok 2 - Random subset-sum problem
ok 3 - Random subset-sum problem
ok 4 - Random subset-sum problem

Referencias

Para saber mas sobre Test::LectroTest lea las traparencias de Tim Moertel en http://community.moertel.com/~thor/talks/pgh-pm-talk-lectrotest.pdf .



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