使用编译测试节省时间

在过去的一年里,我一直在从事几个大型Perl项目,有时是团队项目,有时是个人项目。随着代码库的增长,测试变得越来越重要,其中一种特别有益的测试是编译测试。也就是说,在运行其他任何测试之前,只需检查代码库中的每个模块是否都能编译。

基础知识

让我们看看一个简单的编译测试,这个例子是从Perly-Bot中改编的。

#!/usr/bin/env perl
use Test::More;
use lib 'lib';

my @modules = qw(
  Perly::Bot
  Perly::Bot::Feed
  Perly::Bot::Feed::Post
  Perly::Bot::Cache
  Perly::Bot::Media
  Perly::Bot::Media::Twitter
  Perly::Bot::Media::Reddit
);
for my $module ( @modules )
{
  BAIL_OUT( "$module does not compile" ) unless require_ok( $module );
}
done_testing();

代码很简单;它将本地lib目录添加到Perl搜索模块的目录列表中。然后它声明一个名为@modules的模块名称数组。最后,它遍历每个模块名称并尝试导入它,如果任何模块加载失败则退出。由于测试通常是按字母顺序运行的,因此这个文件被称为00-compile.t,以便它首先运行。我可以在终端运行这个测试。

$ ./t/00-compile.t
perl t/00-compile.t 
ok 1 - use Perly::Bot;
ok 2 - use Perly::Bot::Feed;
ok 3 - use Perly::Bot::Feed::Post;
ok 4 - use Perly::Bot::Cache;
ok 5 - use Perly::Bot::Media;
ok 6 - use Perly::Bot::Media::Twitter;
ok 7 - use Perly::Bot::Media::Reddit;
1..7

一次写入的编译测试

基本的编译测试示例有一个明显的缺陷:它要求程序员列出所有要测试的模块名称。这意味着每次向代码库中添加新模块或重命名模块时,都需要更新这个测试。这也引入了错误的风险 - 一个失败的模块可能存在于代码库中,但从未被测试。而不是静态的模块列表,我可以告诉Perl搜索lib目录并尝试导入它找到的任何模块。

#!/usr/bin/env perl
use Test::More;
use lib 'lib';
use Path::Tiny;

# try to import every .pm file in /lib
my $dir = path('lib/');
my $iter = $dir->iterator({
            recurse         => 1,
            follow_symlinks => 0,
           }); 
while (my $path = $iter->())
{
  next if $path->is_dir || $path !~ /\.pm$/;
  BAIL_OUT( "$path does not compile" ) unless require_ok( $path );
}
done_testing;

在这里,我使用Path::Tiny遍历lib目录中的文件。我不传递模块名称,而是将文件路径传递给require_ok。现在这个编译测试是动态的,它将始终选择代码库中添加或删除的新模块。太棒了!

要求警告

使用require通过文件路径而不是模块名称加载文件的一个问题是,如果同一模块被不同的文件加载两次,它可能会生成“子程序重新定义”警告。想象一下这个代码

require 'lib/Game.pm';
require 'lib/Game/Asset/Player.pm';

如果Game.pm加载Game::Asset::Player,当第二个require语句执行时,Perl会发出“子程序重新定义”警告。我可以通过几种方式处理这个问题:我可以在编译测试文件中添加no warnings 'redefine';来抑制警告。但这样会掩盖可能是有用的真实警告,比如我在代码库中有循环依赖。或者我可以将文件路径转换为模块名称,然后require就不会抱怨,例如

require 'Game';
require 'Game::Asset::Player';

对于编译测试,我可以用替换正则表达式将文件路径转换为模块名称。当编译测试运行时,它们不会生成虚假的“子程序重新定义”警告。

#!/usr/bin/env perl
use Test::More;
use lib 'lib';
use Path::Tiny;

# try to import every .pm file in /lib
my $dir = path('lib/');
my $iter = $dir->iterator({
            recurse         => 1,
            follow_symlinks => 0,
           });
while (my $path = $iter->())
{
  next if $path->is_dir || $path !~ /\.pm$/;
  my $module = $path->relative;
  $module =~ s/(?:^lib\/|\.pm$)//g;
  $module =~ s/\//::/g;
  BAIL_OUT( "$module does not compile" ) unless require_ok( $module );
}
done_testing;

其他想法

编写编译测试的另一种方法是使用Class::Load进行模块导入。它有几个用于动态加载模块的有用函数。

编译测试是测试类中的一个有趣类别。它是公理“代码库应始终可编译”的实现。根据应用程序的不同,您还可以测试其他公理。例如,对于Web应用程序,每个管理员URL只应允许经过认证和授权的用户访问。因此,您可以编写一个动态测试,列出每个管理员URL并尝试未经授权地获取它(如果任何请求成功,则测试失败)。对于测试Catalyst Web应用程序,您可能会发现我的模块Catalyst::Plugin::ActionPaths很有用。测试公理通常具有高回报,维护成本低或几乎没有。寻找它们吧!

如果您需要抑制特定的警告,在Perl的新版本中,warnings pragama 文档列出了它识别的所有警告类型。当使用如子程序签名这样的实验性功能时,这特别有用。您可以在命令行中使用以下命令阅读您版本Perl的版本:

$ perldoc warnings


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

标签

David Farrell

David是一位职业程序员,他经常推文博客有关代码和编程艺术的见解。

浏览他们的文章

反馈

这篇文章有什么问题吗?通过在GitHub上打开问题或拉取请求来帮助我们。