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;
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" testComo 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
:
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).
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 filesEn 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;
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
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-212Una 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 1977de 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'
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'
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'
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'
[$Capacity, [$w_0, ..., $w_n], [$p_0, ..., $p_n]]
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
Para saber mas sobre Test::LectroTest lea las traparencias de Tim Moertel en http://community.moertel.com/~thor/talks/pgh-pm-talk-lectrotest.pdf .