Test::Base(3)
πΈ NAME πΈ
Test::Base β A Data Driven Testing Framework
Okay bestie, this is the manual page for Test::Base(3) from User Contributed Perl Documentation. It is literally a data driven testing framework, and it is about to make your test files feel way less crusty. β¨
β¨ SYNOPSIS β¨
A new test module:
# lib/MyProject/Test.pm
package MyProject::Test;
use Test::Base -Base;
use MyProject;
package MyProject::Test::Filter;
use Test::Base::Filter -base;
sub my_filter {
return MyProject->do_something(shift);
}
A sample test:
# t/sample.t
use MyProject::Test;
plan tests => 1 * blocks;
run_is input => 'expected';
sub local_filter {
s/my/your/;
}
__END__
=== Test one (the name of the test)
--- input my_filter local_filter
my
input
lines
--- expected
expected
output
=== Test two
This is an optional description
of this particular test.
--- input my_filter
other
input
lines
--- expected
other expected
output
π¦ DESCRIPTION π¦
Testing is usually the ugly part of Perl module authoring. Perl gives you a standard way to run tests with Test::Harness, and basic testing primitives with Test::More. After that you are pretty much on your own to develop a testing framework and philosophy. Test::More encourages you to make your own framework by subclassing Test::Builder, but that is not trivial.
Test::Base gives you a way to write your own test framework base class that is trivial. In fact it is as simple as two lines:
package MyTestFramework;
use Test::Base -Base;
A module called MyTestFramework.pm containing those two lines will give all the power of Test::More and all the power of Test::Base to every test file that uses it. As you build up the capabilities of MyTestFramework, your tests will have all of that power as well.
MyTestFramework becomes a place for you to put all of your reusable testing bits. As you write tests, you will see patterns and duplication, and you can "upstream" them into MyTestFramework. Of course, you don't have to subclass Test::Base at all. You can use it directly in many applications, including everywhere you would use Test::More.
Test::Base concentrates on offering reusable data driven patterns, so that you can write tests with a minimum of code. At the heart of all testing you have inputs, processes and expected outputs. Test::Base provides some clean ways for you to express your input and expected output data, so you can spend your time focusing on that rather than your code scaffolding. π
π EXPORTED FUNCTIONS π
Test::Base extends Test::More and exports all of its functions. So you can basically write your tests the same as Test::More. Test::Base also exports many functions of its own:
is(actual, expected, [test-name])β This is the equivalent ofTest::More'sisfunction with one interesting twist. If your actual and expected results differ and the output is multi-line, this function will show you a unified diff format of output. Consider the benefit when looking for the one character that is different in hundreds of lines of output!
Diff output requires the optional Text::Diff CPAN module. If you don't have this module, the is() function will simply give you normal Test::More output. To disable diffing altogether, set the TEST_SHOW_NO_DIFFS environment variable (or $ENV{TEST_SHOW_NO_DIFFS}) to a true value. You can also call the no_diff function as a shortcut.
blocks( [data-section-name] )β The most important function isblocks. In list context it returns a list ofTest::Base::Blockobjects that are generated from the test specification in theDATAsection of your test file. In scalar context it returns the number of objects. This is useful to calculate yourTest::Moreplan.
Each Test::Base::Block object has methods that correspond to the names of that object's data sections. There is also a name and a description method for accessing those parts of the block if they were specified.
The blocks function can take an optional single argument that indicates to only return the blocks that contain a particular named data section. Otherwise blocks returns all blocks.
my @all_of_my_blocks = blocks;
my @just_the_foo_blocks = blocks('foo');
next_block()β You can use thenext_blockfunction to iterate over all the blocks.
while (my $block = next_block) {
...
}
It returns undef after all blocks have been iterated over. It can then be called again to reiterate.
first_block()β Returns the first block orundefif there are none. It resets the iterator to thenext_blockfunction.run(&subroutine)β There are many ways to write your tests. You can reference each block individually or you can loop over all the blocks and perform a common operation. Therunfunction does the looping for you, so all you need to do is pass it a code block to execute for each block.
The run function takes a subroutine as an argument, and calls the sub one time for each block in the specification. It passes the current block object to the subroutine.
run {
my $block = shift;
is(process($block->foo), $block->bar, $block->name);
};
run_is([data_name1, data_name2])β Many times you simply want to see if two data sections are equivalent in every block, probably after having been run through one or more filters. With therun_isfunction, you can just pass the names of any two data sections that exist in every block, and it will loop over every block comparing the two sections.
run_is 'foo', 'bar';
If no data sections are given run_is will try to detect them automatically.
NOTE: Test::Base will silently ignore any blocks that don't contain both sections.
is_deep($data1, $data2, $test_name)β LikeTest::More'sis_deeplybut uses the more correctTest::Deepmodule.run_is_deeply([data_name1, data_name2])β Likerun_is_deeplybut usesis_deepwhich uses the more correctTest::Deep.run_is_deeply([data_name1, data_name2])β Likerun_isbut usesis_deeplyfor complex data structure comparison.run_is_deeply([data_name1, data_name2])β Likerun_is_deeplybut usesis_deepwhich uses the more correctTest::Deep.
(yes, bestie, this signature appears three times in this page β every single one is staying π )
run_like([data_name, regexp | data_name]);β Therun_likefunction is similar torun_isexcept the second argument is a regular expression. The regexp can either be aqr{}object or a data section that has been filtered into a regular expression.
run_like 'foo', qr{<html.*};
run_like 'foo', 'match';
run_unlike([data_name, regexp | data_name]);β Therun_unlikefunction is similar torun_like, except the opposite.
run_unlike 'foo', qr{<html.*};
run_unlike 'foo', 'no_match';
run_compare(data_name1, data_name2)β Therun_comparefunction is like therun_is,run_is_deeplyand therun_likefunctions all rolled into one. It loops over each relevant block and determines what type of comparison to do.
NOTE: If you do not specify either a plan, or run any tests, the run_compare function will automatically be run.
delimiters($block_delimiter, $data_delimiter)β Override the default delimiters of===and---.spec_file($file_name)β By default,Test::Basereads its input from theDATAsection. This function tells it to get the spec from a file instead.spec_string($test_data)β By default,Test::Basereads its input from theDATAsection. This function tells it to get the spec from a string that has been prepared somehow.filters( @filters_list or $filters_hashref )β Specify a list of additional filters to be applied to all blocks. SeeFILTERSbelow.
You can also specify a hash ref that maps data section names to an array ref of filters for that data type.
filters {
xxx => [qw(chomp lines)],
yyy => ['yaml'],
zzz => 'eval',
};
If a filters list has only one element, the array ref is optional.
filters_delay( [1 | 0] );β By defaultTest::Base::Blockobjects have all their filters run ahead of time. There are testing situations in which it is advantageous to delay the filtering. Calling this function with no arguments or a true value causes the filtering to be delayed.
use Test::Base;
filters_delay;
plan tests => 1 * blocks;
for my $block (blocks) {
...
$block->run_filters;
ok($block->is_filtered);
...
}
In the code above, the filters are called manually, using the run_filters method of Test::Base::Block. In functions like run_is, where the tests are run automatically, filtering is delayed until right before the test.
filter_arguments()β Return the arguments after the equals sign on a filter.
sub my_filter {
my $args = filter_arguments;
# is($args, 'whazzup');
...
}
__DATA__
=== A test
--- data my_filter=whazzup
tie_output()β You can captureSTDOUTandSTDERRfor operations with this function:
my $out = '';
tie_output(*STDOUT, $out);
print "Hey!\n";
print "Che!\n";
untie *STDOUT;
is($out, "Hey!\nChe!\n");
no_diff()β Turn off diff support foris()in a test file.default_object()β Returns the defaultTest::Baseobject. This is useful if you feel the need to do an OO operation in otherwise functional test code. See OO below.WWW() XXX() YYY() ZZZ()β These debugging functions are exported from theSpiffy.pmmodule. See Spiffy for more info.croak() carp() cluck() confess()β You can use the functions from theCarpmodule without needing to import them.Test::Basedoes it for you by default.
β¨ TEST SPECIFICATION β¨
okay so here's the whole situation π Test::Base lets you put your test data wherever you want: in an external file, in the DATA section of your program, or in a scalar variable that's holding all the text input. dealer's choice, bestie β¨
A test specification is just a series of text lines. Each test (or block) gets separated by a line containing the block delimiter plus an optional test "name". Then each block gets further subdivided into named sections β those are marked by a line with the data delimiter and the data section name. And if you want a description of the test? That goes on the lines after the block delimiter but before the first data section. π
Here's the basic layout of a specification:
=== <block name 1>
<optional block description lines>
--- <data section name 1> <filter-1> <filter-2> <filter-n>
<test data lines>
--- <data section name 2> <filter-1> <filter-2> <filter-n>
<test data lines>
--- <data section name n> <filter-1> <filter-2> <filter-n>
<test data lines>
=== <block name 2>
<optional block description lines>
--- <data section name 1> <filter-1> <filter-2> <filter-n>
<test data lines>
--- <data section name 2> <filter-1> <filter-2> <filter-n>
<test data lines>
--- <data section name n> <filter-1> <filter-2> <filter-n>
<test data lines>
And here's an actual code example, because vibes alone don't compile π¦
use Test::Base;
delimiters qw(### :::);
# test code here
__END__
### Test One
We want to see if foo and bar
are really the same...
::: foo
a foo line
another foo line
::: bar
a bar line
another bar line
### Test Two
::: foo
some foo line
some other foo line
::: bar
some bar line
some other bar line
::: baz
some baz line
some other baz line
So this example specifies two blocks. Both of them have foo and bar data sections. The second block also has a baz component. The block delimiter here is ### and the data delimiter is :::. slay π
The default block delimiter is === and the default data delimiter is --- β yes, that trailing space is part of it, don't come for me.
Now, there are some special data section names that exist purely for control purposes:
--- SKIP
--- ONLY
--- LAST
- A block with a
SKIPsection means that test gets ignored. genuinely so useful when you want to temporarily disable a test. π - A block with an
ONLYsection means only that block gets used. this is your bestie when you're hyperfixating on getting a single test to pass. If there's more than one block withONLY, the first one gets chosen. (first come, first served, periodt.)
BecauseONLYis so useful for debugging β and because sometimes you totally forget to remove theONLYflag before committing to the VCS or uploading to CPAN β Test::Base by default gives you a diag message sayingI found ONLY ... maybe you're debugging?. Not a fan? Useno_diag_on_only. π
- A block with a
LASTsection makes that block the last one in the specification. Every block after it gets ignored. byeee π
πΈ FILTERS πΈ
okay this is the part where it gets actually iconic. The real power in writing tests with Test::Base comes from its filtering capabilities. Test::Base ships with an ever growing set of useful generic filters that you can sequence and apply to all your various test blocks. Translation? You can write your block serialization in the most readable format you can find, and then let the filters translate it into whatever you actually need for the test. and writing your own filters is easy too, no gatekeeping here π«Ά
Test::Base lets you specify a list of filters for each data section of each block. The default filters are norm and trim. Those get applied (in order!) to the data after it's been parsed from the specification and before it gets set into its Test::Base::Block object. β¨
You can add to the default filter list with the filters function. And you can specify extra filters for one specific block by listing them after the section name on a data section delimiter line. easy.
Example, because examples ate:
use Test::Base;
filters qw(foo bar);
filters { perl => 'strict' };
sub upper { uc(shift) }
__END__
=== Test one
--- foo trim chomp upper
...
--- bar -norm
...
--- perl eval dumper
my @foo = map {
- $_;
} 1..10;
\ @foo;
Putting a - before a filter on a delimiter line disables that filter. note it, love it.
Scalar vs List π
Each filter can take either a scalar or a list as input, and will return either a scalar or a list. And since filters get chained together, it's genuinely important to learn which filters expect which kind of input and which kind of output. iykyk.
For example, consider this filter list:
norm trim lines chomp array dumper eval
The data always starts out as a single scalar string. So:
normtakes a scalar and returns a scalar.trimtakes a list and returns a list β but a scalar is a valid list, so we're chillin'.linestakes a scalar and returns a list.chomptakes a list and returns a list.arraytakes a list and returns a scalar (an anonymous array reference containing the list elements).dumpertakes a list and returns a scalar.evaltakes a scalar and creates a list.
A list of exactly one element works fine as input to a filter that requires a scalar, but any other list will cause an exception. and a scalar in list context is considered a list of one element. that's the whole rule π
Data accessor methods for blocks will return a list of values when used in list context, and the first element of the list in scalar context. This is usually "the right thing", but be aware. π
The Stock Filters π
Test::Base comes with a large set of stock filters. They live in the Test::Base::Filter module. See Test::Base::Filter for a listing and description of these filters. go look, they're cute.
Rolling Your Own Filters π§
Creating filter extensions is very simple, no cap. You can either write a function in the main namespace, or a method in the Test::Base::Filter namespace or a subclass of it. Either way, the text and any extra arguments get passed in, and you return whatever you want the new value to be. π
Here's a self explanatory example:
use Test::Base;
filters 'foo', 'bar=xyz';
sub foo {
transform(shift);
}
sub Test::Base::Filter::bar {
my $self = shift; # The Test::Base::Filter object
my $data = shift;
my $args = $self->current_arguments;
my $current_block_object = $self->block;
# transform $data in a barish manner
return $data;
}
If you use the method interface for a filter, you can access the block internals by calling the block method on the filter object. π«Ά
Normally you'll probably just use the functional interface, although all the builtin filters are methods. fun fact.
Note that filters defined in themainnamespace can look like:sub filter9 { s/foo/bar/; }since Test::Base automatically munges the input string into the$_variable and checks the return value of the function to see if it looks like a number. If you must define a filter that returns just a single number, do it in a different namespace as a method. These filters don't allow the simplistic$_munging. π
π¦ OO π¦
Test::Base has a nice functional interface for simple usage. But under the hood, everything is object oriented β a default Test::Base object gets created and all the functions are really just method calls on it. the way that works is so satisfying ngl.
Which means: if you need to get fancy, you can absolutely use all the object oriented stuff too. Just create new Test::Base objects and use the functions as methods. π
use Test::Base;
my $blocks1 = Test::Base->new;
my $blocks2 = Test::Base->new;
$blocks1->delimiters(qw(!!! @@@))->spec_file('test1.txt');
$blocks2->delimiters(qw(### $$$))->spec_string($test_data);
plan tests => $blocks1->blocks + $blocks2->blocks;
# ... etc
π THE "TEST::BASE::BLOCK" CLASS π
In Test::Base, blocks get exposed as Test::Base::Block objects. This section lists the methods you can call on a Test::Base::Block object. And of course, every data section name is also available as a method. yes really, every single one. π
name()
This is the optional short description of a block, specified on the block separator line.
description()
This is an optional long description of the block. It's the text taken from between the block separator and the first data section.
seq_num()
Returns a sequence number for this block. Sequence numbers begin with 1. (so not zero-indexed, we're not animals.)
blocks_object()
Returns the Test::Base object that owns this block.
run_filters()
Run the filters on the data sections of the blocks. You don't need to use this method unless you also used the filters_delay function.
is_filtered()
Returns true if filters have already been run for this block.
original_values()
Returns a hash of the original, unfiltered values of each data section.
π SUBCLASSING π
One of the nicest things about Test::Base is that it's easy to subclass. And that's genuinely important, because in your personal project you'll likely want to extend Test::Base with your own filters and other reusable pieces of your test framework. main character energy for your test suite π
Here's an example of a subclass:
package MyTestStuff;
use Test::Base -Base;
our @EXPORT = qw(some_func);
sub some_func {
(my ($self), @_) = find_my_self(@_);
...
}
package MyTestStuff::Block;
use base 'Test::Base::Block';
sub desc {
$self->description(@_);
}
package MyTestStuff::Filter;
use base 'Test::Base::Filter';
sub upper {
$self->assert_scalar(@_);
uc(shift);
}
Note that you don't have to re-Export all the functions from Test::Base. That happens automatically, due to the powers of Spiffy. β¨
And the first line in some_func allows it to be called as either a function or a method in the test code. both? both. π«Ά
π DISTRIBUTION SUPPORT π
You might be thinking "bestie I do not want to use Test::Base in my modules, because it adds an installation dependency." Fear not. Module::Install::TestBase takes care of that for you. π
Just write a Makefile.PL that looks something like this:
use inc::Module::Install;
name 'Foo';
all_from 'lib/Foo.pm';
use_test_base;
WriteAll;
The line with use_test_base will automatically bundle all the code the user needs to run Test::Base based tests. that's it, that's the whole problem solved. π
β OTHER COOL FEATURES β
Test::Base automatically adds:
use strict;
use warnings;
to all of your test scripts and Test::Base subclasses. A Spiffy feature indeed. πΈ
πΈ HISTORY πΈ
okay so storytime, bestie π β this module started its whole life with the horrible and ridicule-inducing name Test::Chunks. Like, not the vibe. Not the moment. π
It got renamed to Test::Base with the hope that it would be seen for the very useful module that it has become. And honestly? Main character energy. π
If you're switching from Test::Chunks to Test::Base, simply substitute the concept and usage of chunks to blocks. That's it. No cap. β¨
π AUTHOR π
Ingy dΓΆt Net <ingy@cpan.org> π«Ά
π COPYRIGHT π
Copyright 2005-2018. Ingy dΓΆt Net.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
See <http://www.perl.com/perl/misc/Artistic.html> π
This iconic manual page came fromperl v5.42.0and it's dated2018-04-20β that'sTest::Base(3), bestie. Periodt. π β¨
π¦ where this page comes from
- opensuse-leap 16.0: perl-Test-Base 0.89-160000.2.2 Β· Artistic-1.0 OR GPL-1.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms