Test2-Tools-xUnit

Perl xUnit framework built on Test2::Workflow


Keywords
perl, perl5, test2, xunit
License
Artistic-1.0-Perl

Documentation

NAME

Test2::Tools::xUnit - Perl xUnit framework built on Test2::Workflow

SYNOPSIS

use Test2::Tools::xUnit;
use Test2::V0;

use DBI;
use Local::SystemUnderTest; # $self->{sys} in this example.

# Tests are indicated with the :Test attribute.  You can make multiple
# assertions per test if desired.  Each method is called on a new instance.
sub run_without_args : Test {
   my $self = shift;
   ok $self->{sys}->run, 'should return true';
}

# Skip tests if you don't want them to be run.
sub self_destruct : Test Skip(unwanted side effects) {
   my $self = shift;
   ok !lives { $self->{sys}->self_destruct() }, "should die";
}

# Todo tests will be run; failures are reported and ignored.
sub frobnicate : Test Todo(not yet implemented) {
   my $self = shift;
   is $self->{sys}->frobnicate(), 42, 'should return 42';
}

# BeforeEach/AfterEach are called once per test method, on an instance.
my $dbh;
sub before_each : BeforeEach {
   my $self = shift;
   $self->{sys} = Local::SystemUnderTest->new( dbh => $dbh );
}

sub after_each : AfterEach {
   my $self = shift;
   $dbh->do("DELETE FROM log");
}

# BeforeAll/AfterAll are called once, as class methods.
sub before_all : BeforeAll {
   $dbh = DBI->connect(...);
}

sub after_all : AfterAll {
   $dbh->disconnect;
}

done_testing;

DESCRIPTION

Test2::Tools::xUnit is an implementation of xUnit for Perl that works with Test2. It uses subroutine attributes to indicate which methods should be called at which times.

By constructing a separate object per test, and randomizing the order in which the tests are called, Test2::Tools::xUnit attempts to provide strong isolation between test methods.

SUBROUTINE ATTRIBUTES

Test

Test indicates that a method should be run as a test.

Any number of assertions can be made within a test method.

Skip(<reason>)

Skip will cause a test method not to be run, but it will be reported as "skipped" in the output.

If omitted, the reason defaults to the name of the test method.

Todo(<reason>)

Todo will cause a test method to be run, but any failure will be reported and ignored.

If omitted, the reason defaults to the name of the test method.

BeforeEach

BeforeEach methods will be run before each test method. They are invoked with the test instance as the first argument, so can store fixtures in the object:

sub before_each : BeforeEach {
    my $self = shift;
    $self->{srv} = Local::Service->new(
        ua => $mock_ua,
    );
}

sub service_run_with_foo_bar : Test {
    my $self = shift;
    my $res = $self->{srv}->run(foo => 'bar');

    # ...make assertions about the response...
}
AfterEach

AfterEach methods will be run after each test method.

BeforeAll

BeforeAll methods will be run once before any tests are started; it runs as a class method before any test instances are constructed, so cannot modify any instance attributes. It can, however, modify package variables, and these will be available to all tests:

my $dbh;

sub before_all : BeforeAll {
    $dbh = DBI->connect(...);
    # ...other db setup...
}

This is useful for setting up expensive fixtures such as databases, although you must take care to reset their state between each test; probably in an AfterEach method.

Because of the risk of breaking test isolation, BeforeAll should be used sparingly; if BeforeEach would work, use that instead.

AfterAll

AfterAll methods will be run after all tests in the class have completed. Similarly to BeforeAll, this will be invoked as a class method.

sub after_all : AfterAll {
    $dbh->disconnect;
}

FEATURES

Integration with Test2

Rather than needing a separate test runner, all Test2::Tools::xUnit tests will be run when done_testing is seen.

Each test method creates a Test2 Subtest.

Randomization of test order

Test methods within a class are called in a random order, to help avoid tests accidentally depending on side-effects of other test methods.

Note that if you use Test2::Plugin::SRand (e.g. via Test2::V0), then the random order will be dependent on the seed chosen.

Lack of inheritance

Note that your test classes should not inherit from Test2::Tools::xUnit; importing the module is enough to make the subroutine attributes work.

This behaviour differs from Test::Class, Test::Class::Moose and older implementations of xUnit in other languages.

Default object constructor

If your test class has a 'new' method, it will be called to construct instances of the class.

If no such method exists, Test2::Tools::xUnit will bless a hash into the calling package for you. For example, this test passes:

package Foo;

use Test2::Tools::xUnit;
use Test2::V0;
use Scalar::Util qw(reftype blessed);

sub called_with_reference_to_blessed_hash : Test {
    my $self = shift;
    is blessed($self), 'Foo';
    is reftype($self), 'HASH';
}

done_testing;

Per-method test instance lifecycle

Test2::Tools::xUnit creates a new object per test method, to promote isolation of tests. The following tests both pass, regardless of the order in which they are called:

use Test2::Tools::xUnit;
use Test2::V0;

sub new { bless { list => [] }, shift }

sub add_one_to_list : Test {
    my $self = shift;
    push @{$self->{list}}, "one";
    is @{$self->{list}}, 1, "list should have one element";
}

sub check_list_is_empty : Test {
    is @{shift->{list}}, 0, "list should be empty";
}

done_testing;

This behaviour is shared with JUnit, but differs from some other xUnit implementations such as NUnit or Test::Class. For more discussion, see https://martinfowler.com/bliki/JunitNewInstance.html

Choice of object framework

Test2::Tools::xUnit will work with your choice of Perl object system, or none. Here is a fictitious example using Moo:

package Calculator::Test;

use Moo;
use Test2::Tools::xUnit;
use Test2::V0;
use Calculator;

has 'calc' => ( is => 'ro', default => sub { Calculator->new } );

sub addition : Test {
    my $result = shift->calc->add(2, 2);
    is $result, 4, "2 + 2 should equal 4";
}

done_testing;

The default value of the calc attribute combines with the lifecycle described above to make a concise alternative to a BeforeEach setup method.

TESTING ADVICE

Entire books have been written on testing; we cannot reproduce all their advice here. However, here are some brief guidelines that might be relevant specifically to xUnit.

One test method per behaviour

Avoid naming test methods after the function they are testing, and then attempting to test all the behaviours in that one method. You should write one test method per behaviour.

This guideline doesn't stop you having multiple assertions per behaviour, or using table-driven tests as in this example:

use Test2::Tools::xUnit;
use Test2::V0;

sub square_root_of_perfect_square : Test {
    my %squares = ( 0 => 0, 1 => 1, 4 => 2, 9 => 3 );
    while (my ($square, $root) = each %squares) {
        my $result = sqrt($square);
        is $result, $root, "should return $root as root of $square";
    }
}

sub square_root_of_negative_square : Test {
    my $result = dies { sqrt(-4) };
    is $result, qr/^Can't take sqrt of -4/, "should throw an exception";
}

done_testing;

Negative numbers were treated as a separate behaviour here, rather than attempting to represent the exception as part of the %squares hash.

Test naming

When failures occur, it helps to have clearly-named tests, to tell you unambiguously what has gone wrong.

For example, the following failure could be misread as saying an exception was thrown unexpectedly:

not ok 1 - square_root_of_negative_square {
    not ok 1 - throws an exception
    1..1
}

Try to include both the test condition and the expected behaviour in your test names, and to make use of the word "should" somewhere too.

This produces output which is harder to misread:

not ok 1 - square_root_of_negative_square {
    not ok 1 - should throw an exception
    1..1
}

If you prefer to use many test classes

Test2::Tools::xUnit will work with multiple *.t files, each with done_testing at the end. However, some people prefer to adopt a module structure to share compilation time across your tests, with an approach such as this:

# In t/lib/Local/FooBar/Test.pm
package Local::FooBar::Test;
use Test2::Tools::xUnit;

...

1;

# In t/run.t
use File::Find;
use Test2::Tools::Basic;

find({
    wanted => sub {require},
    no_chdir => 1,
}, './t/lib');

done_testing;

Extending this example to support running individual test modules is left as an exercise for the reader.

COMPATIBILITY

Test2::Tools::xUnit attempts to co-operate with other modules which also define subroutine attributes. However, if another module imports attributes with the same name (e.g. :Test), it's unlikely that both will work correctly!

Currently only Perl 5.12 and higher are supported.

COPYRIGHT AND LICENSE

Copyright © 2018 CV-Library Ltd.

This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.