The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.


Test::WriteVariants - Dynamic generation of tests in nested combinations of contexts


    use Test::WriteVariants;

    my $test_writer = Test::WriteVariants->new();


        # tests we want to run in various contexts
        input_tests => {
            'core/10-foo' => { require => 't/core/10-foo.t' },
            'core/20-bar' => { require => 't/core/20-bar.t' },

        # one or more providers of variant contexts
        variant_providers => [
            sub {
                my ($path, $context, $tests) = @_;
                my %variants = (
                    plain    => $context->new_env_var(MY_MODULE_PUREPERL => 0),
                    pureperl => $context->new_env_var(MY_MODULE_PUREPERL => 1),
                return %variants;
            sub {
                my ($path, $context, $tests) = @_;
                my %variants = map {
                    $_ => $context->new_env_var(MY_MODULE_WIBBLE => $_),
                } 1..3;
                delete $variants{3} if $context->get_env_var("MY_MODULE_PUREPERL");
                return %variants;

        # where to generate the .t files that wrap the input_tests
        output_dir => 't/variants',

When run that generates the desired test variants:

    Writing t/variants/plain/1/core/10-foo.t
    Writing t/variants/plain/1/core/20-bar.t
    Writing t/variants/plain/2/core/10-foo.t
    Writing t/variants/plain/2/core/20-bar.t
    Writing t/variants/plain/3/core/10-foo.t
    Writing t/variants/plain/3/core/20-bar.t
    Writing t/variants/pureperl/1/core/10-foo.t
    Writing t/variants/pureperl/1/core/20-bar.t
    Writing t/variants/pureperl/2/core/10-foo.t
    Writing t/variants/pureperl/2/core/20-bar.t

Here's what t/variants/pureperl/2/core/20-bar.t looks like:

    END { delete $ENV{MY_MODULE_WIBBLE} } # for VMS
    END { delete $ENV{MY_MODULE_PUREPERL} } # for VMS
    require 't/core/20-bar.t';

Here's an example that uses plugins to provide the tests and the variants:

    my $test_writer = Test::WriteVariants->new();

    # gather set of input tests that we want to run in various contexts
    # these can come from various sources, including modules and test files
    my $input_tests = $test_writer->find_input_test_modules(
        search_path => [ 'DBI::TestCase' ]


        # tests we want to run in various contexts
        input_tests => $input_tests,

        # one or more providers of variant contexts
        # (these can be code refs or plugin namespaces)
        variant_providers => [

        # where to generate the .t files that wrap the input_tests
        output_dir => $output_dir,


Test::WriteVariants is a utility to create variants of a common test.

Given the situation - like in DBI where some tests are the same for DBI::SQL::Nano and it's drop-in replacement SQL::Statement. Or a distribution duo having a Pure-Perl and an XS variant - and the same test shall be used to ensure XS and PP version are really drop-in replacements for each other.



    $test_writer = Test::WriteVariants->new(%attributes);

Instanciates a Test::WriteVariants instance and sets the specified attributes, if any.


    $bool = $test_writer->allow_dir_overwrite;

If the output directory already exists when tumble() is called it'll throw an exception (and warn if it wasn't created during the run). Setting allow_dir_overwrite true disables this safety check.


    $bool = $test_writer->allow_file_overwrite;

If the test file that's about to be written already exists then write_output_files() will throw an exception. Setting allow_file_overwrite true disables this safety check.


        input_tests => \%input_tests,
        variant_providers => \@variant_providers,
        output_dir => $output_dir,

Instanciates a Data::Tumbler. Sets its consumer to call:

    $self->write_output_files($path, $context, $payload, $output_dir)

and sets its add_context to call:

    $context->new($context, $item);

and then calls its tumble method:



    $input_tests = $test_writer->find_input_test_modules(
        search_path => ["Helper"],
        search_dirs => "t/lib",
        test_prefix => "Extra::Helper",
        input_tests => $input_tests


Not yet implemented - will file .t files.


    $input_tests = $test_writer->find_input_inline_tests(
        search_patterns => ["*.it"],
        search_dirs     => "t/inl",
        input_tests     => $input_tests


        $input_tests,   # the \%input_tests to add the test module to
        $test_name,     # the key to use in \%input_tests
        $test_spec      # the details of the test file

Adds the $test_spec to %$input_tests keys by $test_name. In other words:

    $input_tests->{ $test_name } = $test_spec;

An exception will be thrown if a test with $test_name already exists in %$input_tests.

This is a low-level interface that's not usually called directly. See "add_test_module".


        $input_tests,     # the \%input_tests to add the test module to
        $module_name,     # the package name of the test module
        $edit_test_name   # a code ref to edit the test module name in $_


        $input_tests,     # the \%input_tests to add the test module to
        $file_name,       # the file name of the test code to inline
        $edit_test_name   # a code ref to edit the test file name in $_


    $providers = $test_writer->normalize_providers($providers);

Given a reference to an array of providers, returns a reference to a new array. Any code references in the original array are passed through unchanged.

Any other value is treated as a package name and passed to Module::Pluggable::Object as a namespace search_path to find plugins. An exception is thrown if no plugins are found.

The corresponding element of the original $providers array is replaced with a new provider code reference which calls the provider_initial, provider, and provider_final methods, if present, for each plugin namespace in turn.

Normal Data::Tumbler provider subroutines are called with these arguments:

    ($path, $context, $tests)

and the return value is expected to be a hash. Whereas the plugin provider methods are called with these arguments:

    ($test_writer, $path, $context, $tests, $variants)

and the return value is ignored. The $variants argument is a reference to a hash that will be returned to Data::Tumbler and which should be edited by the plugin provider method. This allows a plugin to see, and change, the variants requested by any other plugins that have already been run for this provider.


    $test_writer->write_output_files($path, $context, $input_tests, $output_dir);

Writes test files for each test in %$input_tests, for the given $path and $context, into the $output_dir.

The $output_dir, @$path, and key of %$input_tests are concatenated to form a file name. A ".t" is added if not already present.

Calls "get_test_file_body" to get the content of the test file, and then calls "write_file" to write it.


    $test_writer->write_file($filepath, $content);

Throws an exception if $filepath already exists and "allow_file_overwrite" is not true.

Creates $filepath and writes $content to it. Creates any directories that are needed. Throws an exception on error.


    $test_body = $test_writer->get_test_file_body($context, $test_spec);

XXX This should probably be a method call on an object instanciated by the find_input_test_* methods.


Please report any bugs or feature requests to bug-Test-WriteVariants at, or through the web interface at I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.


You can find documentation for this module with the perldoc command.

    perldoc Test::WriteVariants

You can also look for information at:


Tim Bunce, <timb at>

Jens Rehsack, rehsack at


This module has been created to support DBI::Test in design and separation of concerns.


Copyright 2014-2017 Tim Bunce and Perl5 DBI Team.


This program is free software; you can redistribute it and/or modify it under the terms of either:

        a) the GNU General Public License as published by the Free
        Software Foundation; either version 1, or (at your option) any
        later version, or

        b) the "Artistic License" which comes with this Kit.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the Artistic License for more details.