In this post we will look at how to test Perl code using the Test::More module. Like documentation, testing is an essential part of software development. It helps ensure that your code behaves as expected and can catch bugs early in the development process. A bug in programming is an error, flaw, or unintended behaviour in a program that causes it to produce incorrect or unexpected results. Writing tests can help you identify these bugs before they become a problem in any real world production code you may one day write.
Perl has many testing modules available on CPAN, but one of the most commonly used is Test::More. This module provides a simple and flexible way to write tests for your Perl code. It allows you to write tests that check if your code behaves as expected, and it provides a way to report the results of those tests.
Having installed Module::Starter you will have Test::More already available in your local environment. Test::More has many useful functions that can be used to test your code. Some of the most commonly used functions include:
Function | Description |
---|---|
ok |
Checks if a condition is true. |
is |
Checks if two values are equal. |
is_deeply |
Checks if two data structures are deeply equal. |
isnt |
Checks if two values are not equal. |
like |
Checks if a string matches a regex. |
unlike |
Checks if a string does not match a regex. |
SKIP |
Skips a test with a message. |
TODO |
Marks a test as TODO, indicating it is not yet implemented or is expected to fail. |
use_ok |
Checks if a module can be loaded successfully. |
can_ok |
Checks if an object can call a method. |
isa_ok |
Checks if an object is of a specific class. |
cmp_ok |
Compares two values with a given operator. |
diag |
Prints diagnostic messages. |
explain |
Provides a way to explain a test failure. |
done_testing |
Indicates the end of tests. |
This is only a subset of the functions available in Test::More. You can find the full list of functions and their documentation in the Test::More documentation.
Today we will be extending the module we created in the previous post, Module::Test
, first lets examine the tests files which have been created for us by Module::Starter. The test files are located in the t
directory of your module distribution. You will find a file named t/00-load.t
, which is used to test if the module can be loaded successfully. This file should contain the following code:
#!perl
use 5.006;
use strict;
use warnings;
use Test::More;
plan tests => 1;
BEGIN {
use_ok( 'Module::Test' ) || print "Bail out!\n";
}
diag( "Testing Module::Test $Module::Test::VERSION, Perl $], $^X" );
This code is a basic test that checks if the Module::Test
module can be loaded successfully. The use_ok
function is used to check if the module can be loaded, and if it cannot, it will print "Bail out!" and stop the test. The diag
function is used to print diagnostic messages, which can be useful for debugging. The plan tests => 1;
line indicates that we are planning to run one test in this file. The BEGIN
block will also be new to you, this block is executed as soon as the code is compiled, which means that the use_ok
function will be called before any other code in the file. This is useful for ensuring that the module can be loaded before running any other tests. We will cover global phase code blocks in a future post.
You can run this test two ways, either via make by running perl Makefile.pl
, make
and make test
in the root directory of your module, or by using the prove
command. The prove
command is a more modern way to run tests and provides better output formatting. You can run the test using prove -lv t/
, where -l
tells prove to look for the test files in the current directory, and -v
enables verbose output.
When you run the test under prove
, you should see output similar to the following:
$ prove -lv t/
t/00-load.t .......
ok 1 - use Module::Test;
1..1
# Testing Module::Test 0.01, Perl 5.038002, /Users/lnation/perl5/perlbrew/perls/perl-5.38.2/bin/perl
ok
t/manifest.t ......
1..0 # SKIP Author tests not required for installation
skipped: Author tests not required for installation
t/pod-coverage.t ..
1..0 # SKIP Author tests not required for installation
skipped: Author tests not required for installation
t/pod.t ...........
1..0 # SKIP Author tests not required for installation
skipped: Author tests not required for installation
All tests successful.
Files=4, Tests=1, 0 wallclock secs ( 0.01 usr 0.00 sys + 0.06 cusr 0.01 csys = 0.08 CPU)
Result: PASS
The output indicates that the test passed successfully, and it also shows the version of the module and Perl being used. The 1..1
line indicates that one test was run, and the ok 1 - use Module::Test;
line indicates that the test passed. You will also notice that the other tests have been skipped. The t/manifest.t
, t/pod-coverage.t
, and t/pod.t
files are used to test the manifest, pod coverage, and pod formatting of the module, respectively. These tests are not essential for the functionality of the module, but they are useful for ensuring that the module is well documented and follows best practices. For the these tests to run additional modules will need to be installed, these are Test::Manifest
, Test::Pod::Coverage
and Test::Pod
. You can install these modules using cpan following the same step we did for Module::Starter
. Then when running the tests you can either prepend the prove
command with RELEASE_TESTING=1 prove -lv t
. For now if you want to skip this step it's fine, it will not affect the functionality of your module.
Now that we have covered the basics of what exists currently, lets write our first Perl test file. In programming it is good practice to write your tests describing the functionality you want to implement before you actually implement it. This is known as Test Driven Development (TDD). Create a new file in the t
directory called t/01-functionality.t
. This file will contain our first tests. Open the file and add the following code:
#!perl
use 5.006;
use strict;
use warnings;
use Test::More;
plan tests => 7;
use_ok( 'Module::Test' );
is( Module::Test::function1(), 'Hello, World!', 'function1 returns the correct value' );
is_deeply( Module::Test::function2({ name => 'Rex', age => 30 }), { name => 'Rex', age => 30 }, 'function2 returns the correct data structure' );
eval {
Module::Test::function2({ name => 'Rex' });
};
like( $@, qr/age must be a number/, 'function2 throws an error when age is not provided' );
eval {
Module::Test::function2({ age => 30 });
};
like( $@, qr/name must be a string/, 'function2 throws an error when name is not provided' );
eval {
Module::Test::function2({ name => 50, age => 30 });
};
like( $@, qr/name must be a string/, 'function2 throws an error when name is not a string' );
eval {
Module::Test::function2({ name => "Rex", age => "thirty" });
};
like( $@, qr/age must be a number/, 'function2 throws an error when age is not a number' );
done_testing();
What we have done here is to write tests for the two functions which have been autogenerated by Module::Starter, usually you would rename these functions to something more meaningful, but for the sake of this example we will keep them as is. The first test checks if function1
returns the expected value of 'Hello, World!'. The second test checks if function2
returns the expected data structure when provided with a hash reference containing a name and age. We also have tests that check if function2
throws an error when the required parameters are not provided or when they are of the wrong type. The eval
block is used to catch any errors that are thrown by the function, and the like
function is used to check if the error message matches the expected regex pattern qr
lets us compile a regex to match against. is
and is_deeply
are used to check if the returned values match the expected values. The done_testing
function is called at the end to indicate that all tests have been run.
Now that we have written our tests, we can run them using the prove
command as we did before. Run the following command in the root directory of your module:
prove -lv t/01-functionality.t
You should see output similar to the following:
t/01-functionality.t ..
1..2
ok 1 - use Module::Test;
not ok 2 - function1 returns the correct value
# Failed test 'function1 returns the correct value'
# at t/01-functionality.t line 8.
# got: undef
# expected: 'Hello, World!'
not ok 3
# Failed test at t/01-functionality.t line 9.
# Structures begin differing at:
# $got = undef
# $expected = HASH(0x13fb24b48)
not ok 4 - function2 throws an error when age is not provided
# Failed test 'function2 throws an error when age is not provided'
# at t/01-functionality.t line 13.
# ''
# doesn't match '(?^:age must be a number)'
not ok 5 - function2 throws an error when name is not provided
# Failed test 'function2 throws an error when name is not provided'
# at t/01-functionality.t line 17.
# ''
# doesn't match '(?^:name must be a string)'
not ok 6 - function2 throws an error when name is not a string
# Failed test 'function2 throws an error when name is not a string'
# at t/01-functionality.t line 21.
# ''
# doesn't match '(?^:name must be a string)'
not ok 7 - function2 throws an error when age is not a number
# Failed test 'function2 throws an error when age is not a number'
# at t/01-functionality.t line 25.
# ''
# doesn't match '(?^:age must be a number)'
not ok 8 - planned to run 2 but done_testing() expects 7
# Failed test 'planned to run 2 but done_testing() expects 7'
# at t/01-functionality.t line 26.
# Looks like you planned 2 tests but ran 8.
# Looks like you failed 7 tests of 8 run.
Dubious, test returned 7 (wstat 1792, 0x700)
Failed 1/2 subtests
Test Summary Report
-------------------
t/01-functionality.t (Wstat: 1792 (exited 7) Tests: 8 Failed: 7)
Failed tests: 2-8
Non-zero exit status: 7
Parse errors: Bad plan. You planned 2 tests but ran 8.
Files=1, Tests=8, 0 wallclock secs ( 0.01 usr 0.00 sys + 0.01 cusr 0.00 csys = 0.02 CPU)
Result: FAIL
The output indicates that the tests failed, which is expected since we have not yet implemented the functions. The first test failed because function1
returned undef
instead of 'Hello, World!'. The second test failed because function2
did not return the expected data structure. The other tests also failed because the function did not throw the expected errors.
Now that we have written our tests, we can implement the functions to make the tests pass. Open the lib/Module/Test.pm
file and add the following code first to implement function1
:
sub function1 {
return 'Hello, World!';
}
Now if we run prove -lv t/01-functionality.t
again, we should see that the second test passes, but the third test still fails because function2
is not yet implemented.
ok 2 - function1 returns the correct value
Next, we will implement function2
. Add the following code to the lib/Module/Test.pm
file:
sub function2 {
my ($args) = @_;
if (! defined $args->{name} || ref $args->{name} || $args->{name} =~ m/^[^a-zA-Z\s]+$/) {
die "name must be a string";
}
die "age must be a number"
unless (defined $args->{age} && ! ref $args->{age} && $args->{age} =~ m/^[\d]+$/);
return $args;
}
This implementation checks if the name
and age
parameters are provided and of the correct type. If they are not, it throws an error with the appropriate message. If both parameters are valid, it returns the hash reference. I have purposely written the code to do the same thing two different ways, the first I use an if statement to check if the name
parameter is defined, not a reference, and does not contain any non-alphabetic characters. The second check uses a an unless
to ensure that the age
parameter is defined, not a reference, and contains only digits. There is more than one way to write code in Perl, and you will find that different programmers have different styles. The important thing is that the code is readable and maintainable. Also there are plenty of CPAN modules that can help you with validation, we may go into a few in the future.
Next if you run your tests
prove -lv t/01-functionality.t
You should see that all tests pass:
t/01-functionality.t ..
1..7
ok 1 - use Module::Test;
ok 2 - function1 returns the correct value
ok 3 - function2 returns the correct data structure
ok 4 - function2 throws an error when age is not provided
ok 5 - function2 throws an error when name is not provided
ok 6 - function2 throws an error when name is not a string
ok 7 - function2 throws an error when age is not a number
ok
All tests successful.
Files=1, Tests=7, 0 wallclock secs ( 0.01 usr 0.00 sys + 0.01 cusr 0.00 csys = 0.02 CPU)
Result: PASS
Next update your POD, describing above each function what the task they now perform, you may also want to update the SYNOPSIS. This step may or may not be useful to your future self.
If you followed along, you have now written your first Perl tests and implemented the code to make them pass. This is a great start in understanding how to write tests in Perl using the Test::More module. Testing is an essential part of software development, and writing tests can help you catch bugs early in the development process.
In the next post, we will look at how to export functions from your module so that you do not need to call them with the fully qualified name. Until then, keep practicing writing tests and implementing code to make them pass. Testing is a skill that will serve you well as you continue your journey in Perl programming. If you have any questions or comments, feel free to leave them below.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.