From bf8c97af82b02bdcc5adeabef93dcf7eb94748f3 Mon Sep 17 00:00:00 2001 From: Yohann Dufour Date: Fri, 18 Jul 2014 16:23:07 +0200 Subject: [PATCH] Bug 12603: creating TestBuilder to simplify tests This patch contains a new module t::lib::TestBuilder which allows to write tests easier and it contains the unit tests of this module. For more information, see the documentation of the module. This module uses the DBIx::Class schema and works with a clean DBIx::Class schema. In order to use it, you have to remove the current circular dependence (existing in the DBIx::Class) by applying the last patch of the bug 11518. --- t/db_dependent/TestBuilder.t | 249 +++++++++++++++++++++++++++ t/lib/TestBuilder.pm | 397 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 t/db_dependent/TestBuilder.t create mode 100644 t/lib/TestBuilder.pm diff --git a/t/db_dependent/TestBuilder.t b/t/db_dependent/TestBuilder.t new file mode 100644 index 0000000..6110bea --- /dev/null +++ b/t/db_dependent/TestBuilder.t @@ -0,0 +1,249 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright 2014 - Biblibre SARL +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha 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 the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 40; + +BEGIN { + use_ok('t::lib::TestBuilder'); +} + + +my $builder = t::lib::TestBuilder->new(); + +is( $builder->build(), undef, 'build without arguments returns undef' ); + +my @sources = $builder->schema->sources; +my $nb_failure = 0; +for my $source (@sources) { + eval { $builder->build( { source => $source } ); }; + $nb_failure++ if ($@); +} +is( $nb_failure, 0, 'TestBuilder can create a entry for every sources' ); + +my $my_overduerules_transport_type = { + message_transport_type => { + message_transport_type => 'my msg_t_t', + }, + letternumber => 1, + branchcode => { + branchcode => 'codeB', + categorycode => 'codeC', + }, + categorycode => undef, +}; +$my_overduerules_transport_type->{categorycode} = $my_overduerules_transport_type->{branchcode}; +my $overduerules_transport_type = $builder->build({ + source => 'OverduerulesTransportType', + value => $my_overduerules_transport_type, +}); +is( + $overduerules_transport_type->{message_transport_type}, + $my_overduerules_transport_type->{message_transport_type}->{message_transport_type}, + 'build stores the message_transport_type correctly' +); +is( + $overduerules_transport_type->{letternumber}, + $my_overduerules_transport_type->{letternumber}, + 'build stores the letternumber correctly' +); +is( + $overduerules_transport_type->{branchcode}, + $my_overduerules_transport_type->{branchcode}->{branchcode}, + 'build stores the branchcode correctly' +); +is( + $overduerules_transport_type->{categorycode}, + $my_overduerules_transport_type->{categorycode}->{categorycode}, + 'build stores the categorycode correctly' +); +is( + $overduerules_transport_type->{_fk}->{message_transport_type}->{message_transport_type}, + $my_overduerules_transport_type->{message_transport_type}->{message_transport_type}, + 'build stores the foreign key message_transport_type correctly' +); +is( + $overduerules_transport_type->{_fk}->{branchcode}->{branchcode}, + $my_overduerules_transport_type->{branchcode}->{branchcode}, + 'build stores the foreign key branchcode correctly' +); +is( + $overduerules_transport_type->{_fk}->{categorycode}->{categorycode}, + $my_overduerules_transport_type->{categorycode}->{categorycode}, + 'build stores the foreign key categorycode correctly' +); +is_deeply( + $overduerules_transport_type->{_fk}->{branchcode}, + $overduerules_transport_type->{_fk}->{categorycode}, + 'build links the branchcode and the categorycode correctly' +); +isnt( + $overduerules_transport_type->{_fk}->{branchcode}->{letter2}, + undef, + 'build generates values if they are not given' +); + +my $my_user_permission = $t::lib::TestBuilder::default_value->{UserPermission}; +my $user_permission = $builder->build({ + source => 'UserPermission', +}); +isnt( + $user_permission->{borrowernumber}, + undef, + 'build generates a borrowernumber correctly' +); +is( + $user_permission->{module_bit}, + $my_user_permission->{module_bit}->{module_bit}->{bit}, + 'build stores the default value correctly' +); +is( + $user_permission->{code}, + $my_user_permission->{module_bit}->{code}, + 'build stores the default value correctly' +); +is( + $user_permission->{borrowernumber}, + $user_permission->{_fk}->{borrowernumber}->{borrowernumber}, + 'build links the foreign key correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{surname}, + $my_user_permission->{borrowernumber}->{surname}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{address}, + $my_user_permission->{borrowernumber}->{address}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{city}, + $my_user_permission->{borrowernumber}->{city}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchcode}, + $my_user_permission->{borrowernumber}->{branchcode}->{branchcode}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchname}, + $my_user_permission->{borrowernumber}->{branchcode}->{branchname}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{categorycode}, + $my_user_permission->{borrowernumber}->{categorycode}->{categorycode}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{hidelostitems}, + $my_user_permission->{borrowernumber}->{categorycode}->{hidelostitems}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{category_type}, + $my_user_permission->{borrowernumber}->{categorycode}->{category_type}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{defaultprivacy}, + $my_user_permission->{borrowernumber}->{categorycode}->{defaultprivacy}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{borrowernumber}->{privacy}, + $my_user_permission->{borrowernumber}->{privacy}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{module_bit}->{_fk}->{module_bit}->{bit}, + $my_user_permission->{module_bit}->{module_bit}->{bit}, + 'build stores the foreign key value correctly' +); +is( + $user_permission->{_fk}->{module_bit}->{code}, + $my_user_permission->{module_bit}->{code}, + 'build stores the foreign key value correctly' +); +is_deeply( + $user_permission->{_fk}->{module_bit}, + $user_permission->{_fk}->{code}, + 'build links the codes correctly' +); +isnt( + $user_permission->{_fk}->{borrowernumber}->{cardnumber}, + undef, + 'build generates values if they are not given' +); +isnt( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchaddress1}, + undef, + 'build generates values if they are not given' +); +isnt( + $user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{description}, + undef, + 'build generates values if they are not given' +); +isnt( + $user_permission->{_fk}->{module_bit}->{description}, + undef, + 'build generates values if they are not given' +); +isnt( + $user_permission->{_fk}->{module_bit}->{_fk}->{module_bit}->{flag}, + undef, + 'build generates values if they are not given' +); + + +my $nb_basket = $builder->schema->resultset('Aqbasket')->search(); +isnt( $nb_basket, 0, 'add stores the generated entries correctly' ); +$builder->clear( { source => 'Aqbasket' } ); +$nb_basket = $builder->schema->resultset('Aqbasket')->search(); +is( $nb_basket, 0, 'clear removes all the entries correctly' ); + + +my $rs_aqbookseller = $builder->schema->resultset('Aqbookseller'); +my $bookseller = $builder->build({ + source => 'Aqbookseller', + only_fk => 1, +}); +delete $bookseller->{_fk}; +my $bookseller_from_db = $rs_aqbookseller->find($bookseller); +is( $bookseller_from_db, undef, 'build with only_fk = 1 does not store the entry' ); +my $bookseller_result = $rs_aqbookseller->create($bookseller); +is( $bookseller_result->in_storage, 1, 'build with only_fk = 1 creates the foreign keys correctly' ); + +$bookseller = $builder->build({ + source => 'Aqbookseller', +}); +delete $bookseller->{_fk}; +$bookseller_from_db = $rs_aqbookseller->find($bookseller); +is( $bookseller_from_db->in_storage, 1, 'build without the parameter only_sk stores the entry correctly' ); + +$bookseller = $builder->build({ + source => 'Aqbookseller', + only_fk => 0, +}); +delete $bookseller->{_fk}; +$bookseller_from_db = $rs_aqbookseller->find($bookseller); +is( $bookseller_from_db->in_storage, 1, 'build with only_fk = 0 stores the entry correctly' ); diff --git a/t/lib/TestBuilder.pm b/t/lib/TestBuilder.pm new file mode 100644 index 0000000..bb3ab74 --- /dev/null +++ b/t/lib/TestBuilder.pm @@ -0,0 +1,397 @@ +package t::lib::TestBuilder; + +use Modern::Perl; +use Koha::Database; +use String::Random; + + +my $gen_type = { + tinyint => \&_gen_int, + smallint => \&_gen_int, + mediumint => \&_gen_int, + integer => \&_gen_int, + bigint => \&_gen_int, + + float => \&_gen_real, + decimal => \&_gen_real, + double_precision => \&_gen_real, + + timestamp => \&_gen_date, + datetime => \&_gen_date, + date => \&_gen_date, + + char => \&_gen_text, + varchar => \&_gen_text, + tinytext => \&_gen_text, + text => \&_gen_text, + mediumtext => \&_gen_text, + longtext => \&_gen_text, + + set => \&_gen_set_enum, + enum => \&_gen_set_enum, + + tinyblob => \&_gen_blob, + mediumblob => \&_gen_blob, + blob => \&_gen_blob, + longblob => \&_gen_blob, +}; + +our $default_value = { + UserPermission => { + borrowernumber => { + surname => 'my surname', + address => 'my adress', + city => 'my city', + branchcode => { + branchcode => 'cB', + branchname => 'my branchname', + }, + categorycode => { + categorycode => 'cC', + hidelostitems => 0, + category_type => 'A', + default_privacy => 'default', + }, + privacy => 1, + }, + module_bit => { + module_bit => { + bit => '10', + }, + code => 'my code', + }, + code => undef, + }, +}; +$default_value->{UserPermission}->{code} = $default_value->{UserPermission}->{module_bit}; + + +sub new { + my ($class) = @_; + my $self = {}; + bless( $self, $class ); + + $self->schema( Koha::Database->new()->schema ); + $self->schema->txn_begin(); + $self->schema->storage->sql_maker->quote_char('`'); + return $self; +} + +sub schema { + my ($self, $schema) = @_; + + if( defined( $schema ) ) { + $self->{schema} = $schema; + } + return $self->{schema}; +} + +sub clear { + my ($self, $params) = @_; + my $source = $self->schema->resultset( $params->{source} ); + return $source->delete_all(); +} + +sub build { + my ($self, $params) = @_; + my $source = $params->{source} || return; + my $value = $params->{value}; + my $only_fk = $params->{only_fk} || 0; + + my $col_values = $self->_buildColumnValues({ + source => $source, + value => $value, + }); + + my $data; + my $foreign_keys = $self->_getForeignKeys( { source => $source } ); + for my $fk ( @$foreign_keys ) { + my $fk_value; + my $col_name = $fk->{keys}->[0]->{col_name}; + if( ref( $col_values->{$col_name} ) eq 'HASH' ) { + $fk_value = $col_values->{$col_name}; + } + elsif( defined( $col_values->{$col_name} ) ) { + next; + } + + my $fk_row = $self->build({ + source => $fk->{source}, + value => $fk_value, + }); + + my $keys = $fk->{keys}; + for my $key( @$keys ) { + $col_values->{ $key->{col_name} } = $fk_row->{ $key->{col_fk_name} }; + $data->{ $key->{col_name} } = $fk_row; + } + } + + my $new_row; + if( $only_fk ) { + $new_row = $col_values; + } + else { + $new_row = $self->_storeColumnValues({ + source => $source, + values => $col_values, + }); + } + $new_row->{_fk} = $data if( defined( $data ) ); + return $new_row; +} + +sub _formatSource { + my ($params) = @_; + my $source = $params->{source}; + $source =~ s|(\w+)$|$1|; + return $source; +} + +sub _buildColumnValues { + my ($self, $params) = @_; + my $source = _formatSource( { source => $params->{source} } ); + my $value = $params->{value}; + + my $col_values; + my @columns = $self->schema->source($source)->columns; + for my $col_name( @columns ) { + my $col_value = $self->_buildColumnValue({ + source => $source, + column_name => $col_name, + value => $value, + }); + $col_values->{$col_name} = $col_value if( defined( $col_value ) ); + } + return $col_values; +} + +# Returns [ { +# rel_name => $rel_name, +# source => $table_name, +# keys => [ { +# col_name => $col_name, +# col_fk_name => $col_fk_name, +# }, ... ] +# }, ... ] +sub _getForeignKeys { + my ($self, $params) = @_; + my $source = $self->schema->source( $params->{source} ); + + my @foreign_keys = (); + my @relationships = $source->relationships; + for my $rel_name( @relationships ) { + my $rel_info = $source->relationship_info($rel_name); + if( $rel_info->{attrs}->{is_foreign_key_constraint} ) { + my $rel = { + rel_name => $rel_name, + source => $rel_info->{source}, + }; + + my @keys = (); + while( my ($col_fk_name, $col_name) = each($rel_info->{cond}) ) { + $col_name =~ s|self.(\w+)|$1|; + $col_fk_name =~ s|foreign.(\w+)|$1|; + push @keys, { + col_name => $col_name, + col_fk_name => $col_fk_name, + }; + } + $rel->{keys} = \@keys; + + push @foreign_keys, $rel; + } + } + return \@foreign_keys; +} + +sub _storeColumnValues { + my ($self, $params) = @_; + my $source = $params->{source}; + my $col_values = $params->{values}; + + my $new_row; + eval { + $new_row = $self->schema->resultset($source)->update_or_create($col_values); + }; + die "$source - $@\n" if ($@); + + eval { + $new_row = { $new_row->get_columns }; + }; + warn "$source - $@\n" if ($@); + return $new_row; +} + +sub _buildColumnValue { + my ($self, $params) = @_; + my $source = $params->{source}; + my $value = $params->{value}; + my $col_name = $params->{column_name}; + my $col_info = $self->schema->source($source)->column_info($col_name); + + my $col_value; + if( exists( $value->{$col_name} ) ) { + $col_value = $value->{$col_name}; + } + elsif( exists( $default_value->{$source}->{$col_name} ) ) { + $col_value = $default_value->{$source}->{$col_name}; + } + elsif( not $col_info->{default_value} and not $col_info->{is_auto_increment} and not $col_info->{is_foreign_key} ) { + eval { + my $data_type = $col_info->{data_type}; + $data_type =~ s| |_|; + $col_value = $gen_type->{$data_type}->( $self, { info => $col_info } ); + }; + die "The type $col_info->{data_type} is not defined\n" if ($@); + } + return $col_value; +} + + +sub _gen_int { + my ($self, $params) = @_; + my $data_type = $params->{info}->{data_type}; + + my $max = 1; + if( $data_type eq 'tinyint' ) { + $max = 127; + } + elsif( $data_type eq 'smallint' ) { + $max = 32767; + } + elsif( $data_type eq 'mediumint' ) { + $max = 8388607; + } + elsif( $data_type eq 'integer' ) { + $max = 2147483647; + } + elsif( $data_type eq 'bigint' ) { + $max = 9223372036854775807; + } + return int( rand($max+1) ); +} + +sub _gen_real { + my ($self, $params) = @_; + my $max = 10 ** 38; + if( defined( $params->{info}->{size} ) ) { + $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]); + } + return rand($max) + 1; +} + +sub _gen_date { + my ($self, $params) = @_; + return $self->schema->storage->datetime_parser->format_datetime(DateTime->now()); +} + +sub _gen_text { + my ($self, $params) = @_; + my $random = String::Random->new( max => $params->{info}->{size} ); + return $random->randregex('[A-Za-z]+[A-Za-z0-9_]*'); +} + +sub _gen_set_enum { + my ($self, $params) = @_; + return $params->{info}->{extra}->{list}->[0]; +} + +sub _gen_blob { + my ($self, $params) = @_;; + return 'b'; +} + + +sub DESTROY { + my $self = shift; + $self->schema->txn_rollback(); +} + + +=head1 NAME + +t::lib::TestBuilder.pm - Koha module to simplify the writing of tests + +=head1 SYNOPSIS + + use t::lib::TestBuilder; + +Koha module to insert the foreign keys automatically for the tests + +=head1 DESCRIPTION + +This module allows to insert automatically an entry in the database. All the database changes are wrapped in a transaction. +The foreign keys are created according to the DBIx::Class schema. +The taken values are the values by default if it is possible or randomly generated. + +=head1 FUNCTIONS + +=head2 new + + $builder = t::lib::TestBuilder->new() + +Constructor - Begins a transaction and returns the object TestBuilder + +=head2 schema + + $schema = $builder->schema + +Getter - Returns the schema of DBIx::Class + +=head2 clear + + $builder->clear({ source => $source_name }) + +=over + +=item C<$source_name> is the name of the source in the DBIx::Class schema (required) + +=back + +Clears all the data of this source (database table) + +=head2 build + + $builder->build({ + source => $source_name, + value => $value, + only_fk => $only_fk, + }) + +=over + +=item C<$source_name> is the name of the source in the DBIx::Class schema (required) + +=item C<$value> is the values for the entry (optional) + +=item C<$only_fk> is a boolean to indicate if only the foreign keys are created (optional) + +=back + +Inserts an entry in the database by instanciating all the foreign keys. +The values can be specified, the values which are not given are default values if they exists or generated randomly. +Returns the values of the entry as a hashref with an extra key : _fk which contains all the values of the generated foreign keys. + +=head1 AUTHOR + +Yohann Dufour + +=head1 COPYRIGHT + +Copyright 2014 - Biblibre SARL + +=head1 LICENSE + +This file is part of Koha. + +Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or (at your option) any later version. + +Koha 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 the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with Koha; if not, see . + +=cut + +1; -- 1.9.1