DEV Community

Cover image for Learning Perl - Testing
LNATION for LNATION

Posted on • Edited on

Learning Perl - Testing

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" );
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!';
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.