On Github kablamo / slides-perl-testing
prove is a tool for running Perl tests
prove # runs tests in 't' and 'xt' directories
prove t # runs tests in 't' directory
prove t/cowboy t/alien
prove -r t/cowboy t/alien # recursive
prove -lvr # with the 'lib' dir, verbose, recursive
prove \ --lib \ # add 'lib' dir to your $PERL5LIB --verbose \ # more output --recurse \ # run tests in the t dir recursively -Pretty \ # use the prove plugin App::Prove::Plugin::retty --jobs 10 \ # run tests in parallel --state=slow # run slowest tests first
save your options in your ~/.proverc
--lib # add 'lib' dir to your $PERL5LIB --verbose # --recurse # run tests in the t dir recursively --state=save # save test info to .prove in the cwd -Pretty # use the prove plugin App::Prove::Plugin::retty
t ├── 00_compile.t # this test runs first ├── data # dir for test data │ ├── alien.json │ └── cowboy.json ├── lib # dir for test libraries │ └── t │ └── Helper.pm # t::Helper ├── Transmogrifier │ ├── In │ │ └── Tiger.t # unit tests for Transmogrifier::In::Tiger │ └── Out │ ├── Alien.t # unit tests for Transmogrifier::Out::Alien │ └── Cowboy.t # unit tests for Transmogrifier::Out::Cowboy └── Transmogrifier.t # integration tests for Transmogrifier.pm xt # extra tests are run only during a release ├── reallyslow.t └── slow.t
ok( $got, $test_name); ok(!$got, $test_name); is ($got, $expected, $test_name); isnt($got, $expected, $test_name); like ($got, qr/expected/, $test_name); unlike($got, qr/expected/, $test_name);
is_deeply($got_complex_thing, $expected_complex_thing, $test_name);
isa_ok($object, $class);
# rare, but used for complex logic: # eg, if I get to this place, pass() pass "this test will always pass"; fail "this test will always fail";
# Used to make sure you meant to stop testing here and # didn't exit/return/die and skip the rest of your tests. done_testing();
SKIP: { skip $why, $how_many unless $have_some_feature; ok $got1, $test_name1; is $got2, $expected, $test_name2; like $got3, qr/$expected/, $test_name3; };
$ prove t/tiger.t t/tiger.t .. ok 1 - created a tiger ok 2 - tiger has sharp teeth ok 3 - tiger has stripes ok 4 # skip third party tests ok 5 # skip third party tests ok 6 # skip third party tests 1..6 ok All tests successful.
use Test::More skip_all => 'third party tests'; # ...test code here... done_testing;
$ prove t/tiger.t t/tiger.t .. 1..0 # SKIP third party tests skipped: third party tests Files=1, Tests=0, 0 wallclock secs ( 0.01 usr 0.01 sys + 0.04 cusr 0.00 csys = 0.06 CPU) Result: NOTESTS
TODO: { local $TODO = $why; is $got, $expected, $test_name; };
$ prove t/tiger.t t/tiger.t .. ok 1 - created a tiger ok 2 - tiger has sharp teeth ok 3 - tiger has stripes not ok 4 - the tiger ate # TODO third party tests # Failed (TODO) test 'the tiger ate' # at t/Transmogrifier/In/Tiger.t line 18. # got: 'not implemented yet at /home/eric/code/slides-testing-in-perl/src/lib/Transmogrifier/In/Tiger.pm line 9. # ' # expected: undef 1..4 ok All tests successful.
Subtests are really useful and are highly recommended
subtest $name => sub { ok $got1, $test_name1; is $got2, $expected, $test_name2; is $got3, $expected, $test_name3; };
Test Anything Protocol
TAP is the stuff your tests output
# comic book noises test sequence commencing # (this line and line before are ignored by the tap parser) ok 1 - zap() not ok 2 - bam() # Failed test 'bam()' # at xt/slow.t line 6. not ok 3 - pow() # TODO not implemented yet # Failed (TODO) test 'pow()' # at xt/slow.t line 10. ok 4 # skip because ComicBook::Noises::Batman is not installed yargle blargle (this line is ignore by the tap parser) ok 5 - kaboom() 1..5
note "here is a note"; # only displayed in verbose output diag "here's what went wrong"; # always displayed explain \%foo; # dump a data structure
Uses Text::Diff to display a diff when the test fails
eq_or_diff $got, "$line1\n$line2\n$line3\n", "testing strings";
$ prove t/boop.t t/boop.t....1..1 not ok 1 - testing strings # Failed test ((eval 2) at line 14) # +---+----------------+----------------+ # | Ln|Got |Expected | # +---+----------------+----------------+ # | 1|this is line 1 |this is line 1 | # * 2|this is line b |this is line 2 * # | 3|this is line 3 |this is line 3 | # +---+----------------+----------------+ # Looks like you failed 1 test of 1.
my $person = make_person(); like $person->{name}, '/^(Mr|Mrs|Miss) \w+ \w+$/', "name ok"; like $person->{phone}, '/^0d{6}$/', "phone ok"; is scalar keys %$person, 2, "number of keys";
my $person = make_person(); if (ref($person) eq "HASH") { like $person->{name}, '/^(Mr|Mrs|Miss) \w+ \w+$/', "name ok"; like $person->{phone}, '/^0d{6}$/', "phone ok"; is scalar keys %$person, 2, "number of keys"; } else { fail "person is not a hash"; }
my $person = make_person(); if (ref($person) eq "HASH") { like $person->{name}, '/^(Mr|Mrs|Miss) \w+ \w+$/', "name ok"; like $person->{phone}, '/^0d{6}$/', "phone ok"; my $cn = $person->{child_names}; if (ref($cn) eq "ARRAY") { foreach my $child (@$cn) { like($child, $name_pat); } } else { fail "child names not an array"; } } else { fail "person not a hash"; }
Testing data structureswith Test::Moreis doable
Testing horrible complex nested data structureswith Test::Moreis painful
my $person = make_person(); my $name_re = re('^(Mr|Mrs|Miss) \w+ \w+$'); cmp_deeply $person, { name => $name_re, phone => re('^0d{6}$'), child_names => array_each($name_re), }, "person ok";
cmp_deeply \@array, [$hash1, $hash2, ignore()], "first 2 elements are as expected, ignoring third element";
my $expected = { name => ignore(), dna => subsetof('human', 'mutant', 'alien'), is_evil => bool(1), weaknesses => set(@weaknesses), # ignores order, duplicates powers => bag('flight', 'invisibility', 'fire'), # ignores order weapons => any( undef, array_each(isa("Weapon")) ); home_planet => all( isa("Planet"), methods( number_of_moons => any(0..100), distance_to_earth => code(sub { return 1 if shift > 0 }), ), ), }; cmp_deeply $superhero, $expected, "superhero hashref looks ok";
Using standard Perl exception syntax
eval { die "Something bad happened" }; warn $@ if $@;
Using Try::Tiny
try { die "Something bad happened" } catch { warn $_ };
Using standard Perl exception syntax
eval { die My::Exception->new( error => 'Something bad happened', request => $request, response => $response, ); }; warn $@->error if $@;
eval { My::Exception->throw( error => 'Something bad happened', request => $request, response => $response, ); }; warn $@->error if $a;
Using Try::Tiny
try { My::Exception->throw( error => 'Something bad happened', request => $request, response => $response, ); } catch { warn $_->error; };
The standard module for testing exceptions
# Check the exception matches a given regex like exception { $obj->method() }, qr/Something bad happened/, "caught the exception I expected";
# Check method() did not die is exception { $obj->method() }, undef, "method() did not die";
# Check method() did not die lives_ok { $obj->method } 'expecting to live';
Note: Test::Fatal is recommended over Test::Exception
use Test::More; use Test::Warnings; like warning { $obj->method() }, qr/BEEP/, 'got a warning from method()';
use Test::More; use Test::Warnings; is_deeply [ warnings { $obj->method() } ], [ 'INFO: BEEP!', 'INFO: BOOP!', ], 'got 2 warnings from method()';
use Test::More; use Test::Warnings; use Test::Deep; cmp_deeply [ warnings { $obj->method() } ], bag( re(qr/BEEP/), re(qr/BOOP/), ), 'got 2 warnings from method()';
Note: Test::Warnings is recommended over Test::Warn
use Test::More; use Test::Warnings; pass 'yay!; done_testing;
$ prove t/test.t ok 1 - yay! ok 2 - no (unexpected) warnings (via done_testing) 1..2 ok All tests successful.
Note: Test::Warnings is recommended over Test::NoWarnings
Searches upward from the calling file for a directory 't' with a 'lib' directory inside it and adds that to the module search path (@INC).
use Test::Lib; use t::Private::Test::Library;
instead of
use FindBin qw($Bin); use lib "$Bin/lib"; use t::Private::Test::Library;
use Test::More; use Test::Cmd; my $test = Test::Cmd->new(prog => '/bin/ls', workdir => ''); # ls -l /var/log $test->run(args => '-l /var/log'); is $test->stdout, $expected_stdout, 'standard out'; is $test->stderr, $expected_stderr, 'standard error'; is $? >> 8, 1, 'exit status'; # ls -a $test->run(args => '-a'); is $test->stdout, $expected_stdout, 'standard out'; is $test->stderr, $expected_stderr, 'standard error'; is $? >> 8, 1, 'exit status'; done_testing;
TIP: You can strip ansi color codes from your program's output using Term::ANSIColor::colorstrip().
Tell test runners what kind of test you are
use Test::More; use Test::DescribeMe qw/extended/; pass "foo"; done_testing;
$ prove t/test.t 1..0 # SKIP Not running extended tests skipped: Not running extended tests
$ EXTENDED_TESTING=1 prove xt/reallyslow.t xt/reallyslow.t .. ok 1 - very very slow test 1..1 ok All tests successful. Files=1, Tests=1, 0 wallclock secs ( 0.01 usr 0.00 sys + 0.03 cusr 0.00 csys = 0.04 CPU) Result: PASS
See also Test::Settings and the Lancaster Consensus
Web servers are event driven.
Event driven code looks weird.
The traditional Plack::Test UI is weird.
When a request happens, a code reference (aka callback) gets run.
The traditional Plack::Test UI (widely used)
use Plack::Test; test_psgi app => My::WebApp->app, client => sub { # $cb is a callback (aka code reference) my $cb = shift; my $req = HTTP::Request->new(GET => "http://localhost/hello"); my $res = $cb->($req); like $res->content, qr/Hello World/; };
$cb is a wrapper around the PSGI app which does the following:
Creates a PSGI env hash out of the HTTP::Request object Runs the PSGI application in-process -- No forking! No network calls! Returns an HTTP::ResponseAlternate UI for Plack::Test (less common)
use Plack::Test; use HTTP::Request::Common; my $app = My::WebApp->app; my $test = Plack::Test->create($app); my $res = $test->request(GET "/"); like $res->content, qr/Hello World/;
No forking.
No network calls.
Makes things very fast and light.
But if you want forking and network calls there are 2 options for you:
Generate reports about test coverage
$ HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lvrGenerate an html report in ./cover_db/coverage.html
$ coverView the report. It looks like this
The report tells you the coverage of each of the following categories as a percentage.
you can type this
use strict; use warnings; use Test::More; use Test::Exception; use Test::Differences; use Test::Deep; use Test::Warn;
or you can type this
use Test::Most;
but
you can type this
use strict; use warnings; use Test::More; use Test::Fatal; use Test::Warnings; use Test::API; use Test::LongString; use Test::Deep; use Test::Pod; use Test::Pod::Coverage; use Test::Version; use Test::Moose; use Test::CleanNamespace; use Test::Benchmark; use Test::Requires; use Test::RequiresInternet; use Test::Without::Module; use Test::DescribeMe; use Test::Lib;
or you can type this (well its roughly equivalent)
use Test::Modern;
but
Test::Modern doesn't work with Test::Pretty
~/code/text-vcard ⚡ prove t/vcard.t t/vcard.t .. # Subtest: output methods ok 1 - as_string() ok 2 - as_file() ok 3 - file contents ok 1..3 ok 1 - output methods # Subtest: simple getters ok 1 - full_name ok 2 - title ok 3 - photo ok 4 - birthday ok 5 - timezone ok 6 - version 1..6 ok 2 - simple getters # Subtest: photo ok 1 - returns a URI::http object ok 2 - returns a URI::http object ok 3 - photo 1..3 ok 3 - photo # Subtest: complex getters ok 1 - family_names() ok 2 - given_names() ok 3 - prefixes ok 4 - suffixes ok 5 - work phone ok 6 - cell phone ok 7 - work address ok 8 - home address ok 9 - work email address ok 10 - home email address 1..10 ok 4 - complex getters 1..4 ok All tests successful. Files=1, Tests=4, 0 wallclock secs ( 0.03 usr 0.00 sys + 0.07 cusr 0.02 csys = 0.12 CPU) Result: PASS
~/code/text-vcard ⚡ prove t/vcard.t output methods ✓ as_string() ✓ as_file() ✓ file contents ok simple getters ✓ full_name ✓ title ✓ photo ✓ birthday ✓ timezone ✓ version photo ✓ returns a URI::http object ✓ returns a URI::http object ✓ photo complex getters ✓ family_names() ✓ given_names() ✓ prefixes ✓ suffixes ✓ work phone ✓ cell phone ✓ work address ✓ home address ✓ work email address ✓ home email address ok
Type this on the command line
prove -Pretty t
Or save some typing by adding this to your ~/.proverc
-Pretty
The standard module for testing exceptions for years
Use Test::Fatal instead!
# Check that the stringified exception matches given regex throws_ok { $obj->method } qr/division by zero/, 'caught exception';
# Check an exception of the given class (or subclass) is thrown throws_ok { $obj->method } 'Error::Simple', 'simple error thrown'; like $@->error, qr/division by zero/;
# Check method() died -- we do not care why # dies_ok() is for lazy people; throws_ok() is best practice dies_ok { $obj->method } 'died as expected';
# Check method() did not die lives_ok { $obj->method } 'lived as expected';