Unit Tests

From Koha Wiki
Jump to navigation Jump to search

In general

A unit test is a piece of code that test a little part of code (input / outputs). It is a must have practice used in software that provide more quality and consistency to the code. Have a look to eXtreme programming for example.

This subject is close to Continuous_Integration.

In Koha

Tests generally use Test::More and are kept in two folders:

  • t stands for tests on code and unit tests
    • "real" unit tests compare got and expected value of subs
    • first level is safe for launching everywhere
    • in db_dependent be careful, it affects your current database in $KOHA_CONF
    • Have a look to t/00-deprecated.t t/00-load.t t/check_databaseversion.t
    • For more tricky (database) tests have a look to t/db_dependent/XISBN.t t/db_dependent/Barcodes.t
    • TEST_QA must be set to 1 for 00-testcritics.t runs
    • (create_db / clean_db should be the good practice but it does not work)
  • xt stands for authoring code indentation and sysprefs tests.
    • free for all which can help consistencies / syntax (not functional testing)
    • run it anywhere, no data is modified

You can run a test by simply calling it with perl or using prove tool.

Running tests

Howto run a test

prove t/filename.t
perl t/filename.t 

Howto run all tests

make test

Run tests without modifying database (everywhere, safety)

you@local ~/koha $ prove t

Run db dependent tests (/!\ Only on development computer!)

you@local ~/koha $ prove t/db_dependent

Good practices for running tests

Each time you want to submit a patch run:

you@local ~/koha $ prove t
you@local ~/koha $ prove xt

You must have for both:

All tests successful.

Writing tests

We will use

Bug 18812   : SIP Patron status does not respect OverduesBlockCirc
URL         : https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=18812

as an example... this is a two-line change to C4/SIP/ILS/Patron.pm

The logic behind the patch is that unless the system preference OverduesBlockCirc is set to noblock, we need to block borrowers with overdues.

There are three possible values for OverduesBlockCirc: noblock, confirmation, and block -- to cover all of the possible cases, we need to test a patron that has overdues, and a patron that does not have overdues.

Where and how to test

There are some choices to be made here:

  • Does the test touch the database? Yes => test under t/db_dependent. All database dependent tests must run inside of a transaction, so that any database changes will be rolled back after the test completes.
  • Does the module have any tests/ are the changes covered by tests? No => Create an entirely new sub-test. Yes => Talk to QA -- the size and scope of the tests being added may make it worth re-writing existing tests; it's worth soliciting opinions about how to make your tests fit into the existing file before you add more code.

The test file

In our example, we made the change to C4/SIP/ILS/Patron.pm, and we're hitting the database, so our test should go in t/db_dependent/SIP/Patron.t.

#!/usr/bin/perl

# Some tests for SIP::ILS::Patron
# This needs to be extended! Your help is appreciated..

use Modern::Perl;
use Test::More tests => 3;

use Koha::Database;
use t::lib::TestBuilder;
use t::lib::Mocks;
use C4::SIP::ILS::Patron;

my $schema = Koha::Database->new->schema;
$schema->storage->txn_begin;

my $builder = t::lib::TestBuilder->new();
my $patron1 = $builder->build({ source => 'Borrower' });
my $card = $patron1->{cardnumber};

# Check existing card number
my $sip_patron = C4::SIP::ILS::Patron->new( $card );
is( defined $sip_patron, 1, "Patron is valid" );

# Check invalid cardnumber by deleting patron
$schema->resultset('Borrower')->search({ cardnumber => $card })->delete;
my $sip_patron2 = C4::SIP::ILS::Patron->new( $card );
is( $sip_patron2, undef, "Patron is not valid (anymore)" );

subtest "OverduesBlockCirc tests" => sub {

    plan tests => 6;

    my $odue_patron = $builder->build({ source => 'Borrower' });
    my $good_patron = $builder->build({ source => 'Borrower' });
    my $odue = $builder->build({ source => 'Issue', value => {
            borrowernumber => $odue_patron->{borrowernumber},
            date_due => '2017-01-01',
            }
    });
    t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'noblock' );
    my $odue_sip_patron = C4::SIP::ILS::Patron->new( $odue_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, 1, "Not blocked with overdues when set to 'Don't block'");
    $odue_sip_patron = C4::SIP::ILS::Patron->new( $good_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, 1, "Not blocked without overdues when set to 'Don't block'");

    t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
    $odue_sip_patron = C4::SIP::ILS::Patron->new( $odue_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, '', "Blocked with overdues when set to 'Ask for confirmation'");
    $odue_sip_patron = C4::SIP::ILS::Patron->new( $good_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, 1, "Not blocked without overdues when set to 'confirmation'");

    t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
    $odue_sip_patron = C4::SIP::ILS::Patron->new( $odue_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, '', "Blocked with overdues when set to 'Block'");
    $odue_sip_patron = C4::SIP::ILS::Patron->new( $good_patron->{cardnumber} );
    is( $odue_sip_patron->{charge_ok}, 1, "Not blocked without overdues when set to 'Block'");

};

$schema->storage->txn_rollback;

Wrapping the tests inside a transaction

The first action in the test should be

my $schema = Koha::Database->new->schema;
$schema->storage->txn_begin;

and the last action should be

$schema->storage->txn_rollback;

Any database changes between these two bits of code will be rolled back, assuming that $schema->storage->txn_rollback; is actually executed. More on that later.

Using subtests

At the time that the patch was written, there were no tests that had anything to do with circulation, so it made sense to add a new sub-test. This is more modular, and has all of the associated advantages -- variables are well scoped, tests are grouped together in ways that keep things organized, and there's a single pass/fail result for a sub-test block.

Here's what a subtest looks like:

subtest "OverduesBlockCirc tests" => sub {
    plan tests => 6;
    
    # Tests go here
};

Using TestBuilder

TestBuilder is used to create mock data for testing purposes. It only creates data -- there is a distinct module that is used for mocking objects.

In order to use TestBuilder, add the following use statement:

use t::lib::TestBuilder;

Then instantiate a builder object:

my $builder = t::lib::TestBuilder->new();

You only need a single builder object for the entire test file. If one already exists, use it.

In our test, we want to create one patron who has overdue items, and one who does not. The $builder->build() method takes a hashref containing the key source. The value is a Koha::Schema::Result object -- and in this case, we want the singular -- 'Borrower' and 'Issue' rather than 'Borrowers' and 'Issues'.

    my $odue_patron = $builder->build({ source => 'Borrower' }); 
    my $good_patron = $builder->build({ source => 'Borrower' }); 
    my $odue = $builder->build({ source => 'Issue', value => {
            borrowernumber => $odue_patron->{borrowernumber},
            date_due => '2017-01-01',
            }
    });
Setting values in TestBuilder

$builder->build() may also take a value hashref. In this case, we're assigning values to borrowernumber and date_due inside $odue.

TestBuilder data is random

The values assigned by TestBuilder are random junk:

If we add a call to Data::Dumper::Dumper(), we can see what's in $odue_patron

    my $odue_patron = $builder->build({ source => 'Borrower' }); 
    warn Data::Dumper::Dumper( $odue_patron );
    my $good_patron = $builder->build({ source => 'Borrower' }); 
    my $odue = $builder->build({ source => 'Issue', value => {
            borrowernumber => $odue_patron->{borrowernumber},
            date_due => '2017-01-01',
            }
    });


'prove t/db_dependent/SIP/Patron.t
$VAR1 = {
          'altcontactfirstname' => 'DGFMpCGGh3Y5kBpHsejBQrZ4uTOEik4pFdXG5tfJxYnxXt2yBiJyePVcUHDB3KQCr6qB4PReH0E',
          'guarantorid' => 185839039,
          'contactname' => 'zmodeADp14W',
          'mobile' => 'JzMBHbYpk57Jmdk8AeIVq6utwpc3Jo_2qY6PYMLv',
          'contacttitle' => 'nL9hV6MLSmf',
          'borrowernotes' => 'RXkR7DzBe',
          'email' => 'J3LRDjv',
          'dateenrolled' => '2017-06-16',
          'B_phone' => 'sO8',
          'B_state' => 'R',
          'altcontactaddress2' => 'BUP8AXpXt23gcaMWm1CmwrtcMqFTOAOQM8yyfIP0qtV1c_XPkfX_Q2eLVAOK8hHmX5P5XMGtQ1LsSJ6OXAwlMUwmaNUAnbF5gesCd3j13Gl0vGsOMy3gvtDp90G5EYu2y',
          'altcontactaddress3' => 'YcDirxTMaKYDyGYYQZxIqcKgDLCbMFQh1gUbutg_eczbfIr7lePyJcdZlddDa_6fzx07TYWb3',
          'flags' => 961821399,
          'B_streetnumber' => 'OKoyo_in',
          'lost' => 81,
          'opacnote' => 'izlm9X',
          'altcontactcountry' => 'aAPdqi',
          'altcontactaddress1' => 'o8Toq3nIRdKMp9gI01iWhgCX2Y9rovb3874nZRZtLq77C70HdwocZbfRe5OZ8sTMKiiHm3gqh9ov00CQeQCdzHS_F07DysH3np2M_ovg5vMEn6f4hLMFC4RyOCWRIMO76HqGCRhYIszEKaybkm7y3zzJ9FZKWllupc0H3Z8S_DHILk39ui1566kNpJpuTF4x6w4kUO9ChVYbCW2qJRavJT7Vaq',
          'debarredcomment' => 'Absu09XZOPWagwvdi5BgsH_2CSSQAiW1gjuZJvO8GldygXQoRhWx1mWWxv2tmNEmqkNaLDXVnzg_FAcHPSFAU7nF9kya3ATq5xBeXgMS6SjqjNavI2V65MTedfdcDlZbLu4fnhHFpSiXC0J_2oF2DnvEKe1Hlm2ev0GsdtcqncQ46tnA2Celb',
          ...
          'sms_provider_id' => '11'
        };


Data is constrained by its type -- date fields are valid dates, numeric fields are numeric... but everything that isn't specified using the value hashref, is random within the constraint of the data type.

TestBuilder data is recursive

The data created by TestBuilder is recursive -- not only is 'sms_provider_id' => '11' numeric, it also implies that the associated sms_provider object, with id 11 was created at the same time... TestBuilder fills in all of the dependencies to make sure that this is a valid record.

The tricky thing is that the data in the recursively created objects isn't necessarily valid data according to Koha. It's important to remember that, because it can come back and bite you.


Developer handbook