通过表格驱动测试分离数据和操作

如何在不复制大量代码的情况下轻松地在不同的数据上运行相同的测试?如果我按照我通常的套路来做,我会开始写一些代码,然后剪贴粘贴几次。我添加了一些更多的测试,直到我意识到自己弄乱了一团糟。如果我有预见能力,知道我会再次弄乱(果然如此),我就会从一张数据表和一点点遍历它的代码开始。

考虑一个愚蠢且简单的测试例子,测试String::Sprintf的类似sprintf的行为。我可以使用这个模块来创建自己的格式指定符,例如一个用来加逗号的数字。我大部分是从它的文档中偷来的,尽管我也加入了v5.20子程序签名特性v5.14不可破坏替换运算符,因为我喜欢这些特性

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use String::Sprintf;

my $f = String::Sprintf->formatter(
  N => sub {
    my($width, $value, $values, $letter) = @_;
    return commify(sprintf "%${width}f", $value);
  });

say "Numbers are: " . 
  $f->sprintf(
    '%10.2N, %10.2N', 
    12345678.901, 87654.321
  );

sub commify ( $n ) {
  $n =~ s/(\.\d+)|(?<=\d)(?=(?:\d\d\d)+\b)/$1 || ','/rge;
}
Numbers are: 12,345,678.90,   87,654.32

测试这个功能可能产生的混乱从单个输入和输出开始,使用Test::More函数的is

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use Test::More;
    
sub commify ( $n ) {
  $n =~ s/(\.\d+)|(?<=\d)(?=(?:\d\d\d)+\b)/$1 || ','/rge;
}

my $class = 'String::Sprintf';  
use_ok( $class );
    
my $f = String::Sprintf->formatter(
  N => sub {
    my($width, $value, $values, $letter) = @_;
    return commify(sprintf "%${width}f", $value);
  });
    
isa_ok(  $f, $class );
can_ok( $f, 'sprintf' );

is(  $f->sprintf( '%.2N', '1234.56' ), '1,234.56' );

done_testing();

我决定测试另一个值,我认为最简单的方法是复制带有is的该行

is(  $f->sprintf( '%.2N', '1234.56' ), '1,234.56' );
is(  $f->sprintf( '%.2N', '1234' ),    '1,234.00' );

要测试的具体内容并不是这篇文章的重点。我想强调的是围绕它的所有东西。或者,更准确地说,我想淡化围绕它的所有东西。我不得不复制测试,尽管大部分结构都是相同的。

我可以将这些测试转换为包含数据的结构和另一个包含行为的结构

my @data = (
    [ ( 1234.56, '1,234.56' ) ],
    [ ( 1234,    '1,234.00' ) ],
);

foreach my $row ( @data ) {
  is(  $f->sprintf( '%.2N', $row->[0] ), $row->[1] );
}

我可以在@data中添加更多的行,但代码的核心,即那个foreach循环,并没有改变。

我可以改进这一点。到目前为止,我只测试了一个sprintf模板。我可以将它添加到@data中,并使用它为测试创建标签

my $ndot2_f = '%.2N';

my @data = (
    [ $ndot2_f,( 1234.56, '1,234.56' ) ],
    [ $ndot2_f, ( 1234,    '1,234.00' ) ],
);

foreach my $row ( @data ) {
  is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
       "$row->[1] with format $row->[0] returns $row->[2]"
   );
}

我可以添加另一个使用不同格式的测试。如果我像开始时那样继续,这将看起来像是一个新的测试,因为格式改变了。现在,格式只是输入的一部分

my $ndot2_f = '%.2N';

my @data = (
    [ $ndot2_f, ( 1234.56, '1,234.56' ) ],
    [ $ndot2_f, ( 1234,    '1,234.00' ) ],
    [ '%.0N'  , ( 1234.49, '1,234'    ) ],
);

foreach my $row ( @data ) {
  is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
       "$row->[1] with format $row->[0] returns $row->[2]"
  );
}

随着时间的推移,事情变得越来越复杂。如果一个测试失败,我想要一些关于哪个测试失败的额外信息。我会改变遍历表格的方式。在这种情况下,我将使用v5.12特性,它允许对数组使用each,这样我就得到了索引和值

while( my( $index, $row ) = each @data ) {
  is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
       "$index: $row->[1] with format $row->[0] returns $row->[2]"
  );
}

测试行为代码改变了,但我根本不需要修改输入数据。在这个特定的例子中,特定的代码并不重要。这种表格驱动测试将输入和测试分离;这才是你应该注意的。

它可以变得更好。到目前为止,我一直在测试文件中放置所有的输入数据,但现在由于它与测试代码分开,我可以从其他地方获取输入。这可能是一个制表符分隔的值文件

%.2N   1234.56 1,234.56 
%.2N    1234    1,234.00
%.0N    1234.49 1,234

我在测试文件中通过读取和解析外部文件创建@data

open my $test_data_fh, '<', $test_file_name or die ...;

my @data;
while( <$test_data_fh> ) {
  chomp;
  push @data, split /\t/;
}

现在所有的数据都不在测试文件中。而且,简单的文本文件并没有什么特别之处。我可以做一些额外的工作,从Excel文件(可能是商业中最有用的巫师技能)或数据库中获取数据

use DBI;
    
my $dbh = DBI->connect( ... );
my $sth = $dbh->prepare( 'SELECT * FROM tests' );
    
$sth->execute();
    
while( my $row = $sth->fetchrow_arrayref ) {
  state $index = 0;

  is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
       $index++ . ": $row->[1] with format $row->[0] returns $row->[2]"
  );
}

这就是想法。我将数据和测试分开,以给自己一些灵活性。我如何访问数据以及如何测试取决于我特定的难题。


本文最初发布在PerlTricks.com上。

标签

布莱恩·D·福伊

布莱恩·D·福伊是一位Perl培训师和作家,同时也是Perl.com的高级编辑。他是《Mastering Perl》、《Mojolicious Web Clients》、《Learning Perl Exercises》的作者,以及《Programming Perl》、《Learning Perl》、《Intermediate Perl》和《Effective Perl Programming》的合著者。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上通过打开一个issue或pull request来帮助我们:GitHub