Perl 构建、单元测试、代码覆盖率: 一个完整的工作示例

我找到的关于 Perl 构建过程和单元测试以及代码覆盖率的大多数 Stackoverflow 答案只是指向 CPAN 中的文档。指向 CPAN 模块绝对没有错,因为完整的文档应该存放在这里。但是,在许多情况下,我都很难找到完整的工作代码示例。

我一直在因特网上搜索实际的工作代码样本,我可以下载或粘贴到我的 IDE,像典型的教程“ Hello World”示例源代码,但是一个示例,演示了单元测试和代码覆盖率分析的构建过程。是否有人有一个完整的工作项目示例来演示这些技术和过程?

(我确实有一个小的工作示例,我将用它来回答我自己的问题,但可能还有其他 SO 用户拥有比我提出的更好的示例。)

22656 次浏览

It took me a while and it also took me taking small snippets from a number of different sources and melting them together, but I think I have a small working example that sufficiently demonstrates to a Perl newbie the Perl build process including unit testing and code coverage analysis & reporting. (I'm using ActiveState ActivePerl v5.10.0 on a Windows XP Pro PC, Module::Build, Test::More, Devel::Cover)

Start out with a directory for your Perl project and then create a "lib" directory and a "t" directory under your project directory:

HelloPerlBuildWorld
|
|----------> lib
|
|----------> t

In the "lib" directory, create a text file named "HelloPerlBuildWorld.pm". This file is your Perl module that you will be building and testing. Paste the following content into this file:

use strict;
use warnings;
package HelloPerlBuildWorld;


$HelloPerlBuildWorld::VERSION = '0.1';


sub hello {
return "Hello, Perl Build World!";
}


sub bye {
return "Goodbye, cruel world!";
}


sub repeat {
return 1;
}


sub argumentTest {
my ($booleanArg) = @_;


if (!defined($booleanArg)) {
return "null";
}
elsif ($booleanArg eq "false") {
return "false";
}
elsif ($booleanArg eq "true") {
return "true";
}
else {
return "unknown";
}


return "Unreachable code: cannot be covered";
}


1;

In the "t" directory, create a text file named "HelloPerlBuildWorld.t". This file is your unit test script that will attempt to fully test your Perl module above. Paste the following content into this file:

use strict;
use warnings;
use Test::More qw(no_plan);


# Verify module can be included via "use" pragma
BEGIN { use_ok('HelloPerlBuildWorld') };


# Verify module can be included via "require" pragma
require_ok( 'HelloPerlBuildWorld' );


# Test hello() routine using a regular expression
my $helloCall = HelloPerlBuildWorld::hello();
like($helloCall, qr/Hello, .*World/, "hello() RE test");


# Test hello_message() routine using a got/expected routine
is($helloCall, "Hello, Perl Build World!", "hello() IS test");


# Do not test bye() routine


# Test repeat() routine using a got/expected routine
for (my $ctr=1; $ctr<=10; $ctr++) {
my $repeatCall = HelloPerlBuildWorld::repeat();
is($repeatCall, 1, "repeat() IS test");
}


# Test argumentTest()
my $argumentTestCall1 = HelloPerlBuildWorld::argumentTest();
is($argumentTestCall1, "null", "argumentTest() IS null test");


# Test argumentTest("true")
my $argumentTestCall2 = HelloPerlBuildWorld::argumentTest("true");
is($argumentTestCall2, "true", "argumentTest() IS true test");


# Test argumentTest("false")
my $argumentTestCall3 = HelloPerlBuildWorld::argumentTest("false");
is($argumentTestCall3, "false", "argumentTest() IS false test");


# Test argumentTest(123)
my $argumentTestCall4 = HelloPerlBuildWorld::argumentTest(123);
is($argumentTestCall4, "unknown", "argumentTest() IS unknown test");

Now back up in your top level project directory, create a text file named "Build.PL". This file will create your build scripts that you will use later. Paste the following content into this file:

use strict;
use warnings;
use Module::Build;


my $builder = Module::Build->new(
module_name         => 'HelloPerlBuildWorld',
license             => 'perl',
dist_abstract       => 'HelloPerlBuildWorld short description',
dist_author         => 'Author Name <email_addy@goes.here>',
build_requires => {
'Test::More' => '0.10',
},
);


$builder->create_build_script();

That's all the files you need. Now from the command line in the top level project directory, type the following command:

perl Build.PL

You will see something similar to the following:

Checking prerequisites...
Looks good


Creating new 'Build' script for 'HelloPerlBuildWorld' version '0.1'

Now you should be able to run your unit tests with the following command:

Build test

And see something similar to this:

Copying lib\HelloPerlBuildWorld.pm -> blib\lib\HelloPerlBuildWorld.pm
t\HelloPerlBuildWorld....ok
All tests successful.
Files=1, Tests=18,  0 wallclock secs ( 0.00 cusr +  0.00 csys =  0.00 CPU)

To run your unit tests with code coverage analysis, try this:

Build testcover

And you'll see something on the order of this:

t\HelloPerlBuildWorld....ok
All tests successful.
Files=1, Tests=18, 12 wallclock secs ( 0.00 cusr +  0.00 csys =  0.00 CPU)
cover
Reading database from D:/Documents and Settings/LeuchKW/workspace/HelloPerlBuildWorld/cover_db




----------------------------------- ------ ------ ------ ------ ------ ------
File                                  stmt   bran   cond    sub   time  total
----------------------------------- ------ ------ ------ ------ ------ ------
D:/Perl/lib/ActivePerl/Config.pm       0.0    0.0    0.0    0.0    n/a    0.0
D:/Perl/lib/ActiveState/Path.pm        0.0    0.0    0.0    0.0    n/a    0.0
D:/Perl/lib/AutoLoader.pm              0.0    0.0    0.0    0.0    n/a    0.0
D:/Perl/lib/B.pm                      18.6   16.7   13.3   19.2   96.4   17.6
...
[SNIP]
...
D:/Perl/lib/re.pm                      0.0    0.0    0.0    0.0    n/a    0.0
D:/Perl/lib/strict.pm                 84.6   50.0   50.0  100.0    0.0   73.1
D:/Perl/lib/vars.pm                   44.4   36.4    0.0  100.0    0.0   36.2
D:/Perl/lib/warnings.pm               15.3   12.1    0.0   11.1    0.0   12.0
D:/Perl/lib/warnings/register.pm       0.0    0.0    n/a    0.0    n/a    0.0
blib/lib/HelloPerlBuildWorld.pm       87.5  100.0    n/a   83.3    0.0   89.3
Total                                  9.9    4.6    2.8   11.3  100.0    7.6
----------------------------------- ------ ------ ------ ------ ------ ------




Writing HTML output to D:/Documents and Settings/LeuchKW/workspace/HelloPerlBuildWorld/cover_db/coverage.html ...
done.

(Someone please tell me how to configure Cover to ignore all the Perl libraries except and just report back to me on my single file that I wrote. I could not get Cover filtering to work according to the CPAN documentation!)

Now if you refresh your top level directory, you can see a new subdirectory called "cover_db". Go into that directory and double click on the "coverage.html" file to open the code coverage report in your favorite web browser. It gives you a nice color coded hypertext report where you can click on your file name and see detailed statement, branch, condition, subroutine coverage statistics for your Perl module right there in the report next to the actual source code. You can see in this report that we did not cover the "bye()" routine at all and also there is a line of code that is unreachable that was not covered as we expected.

snapshot of code coverage report
(source: leucht.com)

One more thing you can do to help automate this process in your IDE is to make some more "Build.PL" type files that explicitly perform some of the build targets that we did above manually from the command line. For example, I use a "BuildTest.PL" file with the following content:

use strict;
use warnings;
use Module::Build;


my $build = Module::Build->resume (
properties => {
config_dir => '_build',
},
);


$build->dispatch('build');
$build->dispatch('test');

Then I set up my IDE to execute this file (via "perl BuiltTest.PL") with a single mouse click and it automatically runs my unit test code from the IDE instead of me doing it manually from the command line. Replace the "dispatch('test')" with "dispatch('testcover')" for automated code coverage execution. Type "Build help" for a complete list of build targets that are available from Module::Build.

In response to Kurt, I would propose this alternative to his BuiltTest.PL script.

use strict;
use warnings;
use Module::Build;


my $build = Module::Build->resume (
properties => {
config_dir => '_build',
},
);


$build->dispatch('build');
$build->dispatch('test');

It reuses the database build by Build.PL (and thus assumes that already ran).

I cover this in Intermediate Perl as well as Mastering Perl. Kurt, however, has given a nice summary.

I combine all of this into a release script using Module::Release though. I type one command and it all happens.

The fantastically helpful module-starter generates an easy-to-use skeleton project which handles module installation, creation of documentation and a good layout for module files to live in, and -- I think -- code coverage support. It's IMO a great start for any Perl module-related endeavour.

Also: using CPAN-related tools like Module::Build -- even for modules which are likely never going to be released publically -- is a very good idea.

(disclosure: I'm the author)

Once you have everything sorted as described above, you could take the next step and use Devel::CoverX::Covered to e.g.

  • Given a source file, list the test files that provide coverage to that source file. This can be done on a file, sub routine and row level.
  • Given a test file, list the source files and subs covered by that test file.
  • Given a source file, report efficiently on the coverage details per row, or sub.

See the synopsis for concrete command line examples.

In Devel::PerlySense there's Emacs support to display the coverage information in the source code buffer (screen shot), and to navigate to/from covering test files.