testing orm based code
TRANSCRIPT
Testing ORM base code
Viktor TurskyiCTO at WebbyLab
Kiev 2012
Bad News
ORM-based code is database dependent code
ORM framework is a blackbox
Good news
ORM is about objects and classes
We can do testing without object persistency
package Language;
use base qw(Rose::DB::Object);
__PACKAGE__->meta->setup(
table => 'languages',
columns => [
language_id => { type => 'varchar', length => 3, not_null => 1 },
name => { type => 'varchar', length => 64, not_null => 1 },
],
primary_key_columns => [ 'language_id' ],
);
We do not test ORM framework - it has own tests.
Rule 1: No tests for simple classes
Rule 2: Try inject dependenciespackage VAT;
use base qw(Rose::DB::Object);
__PACKAGE__->meta->setup(
table => 'vats',
columns => [
vat_id => { type => 'varchar', length => 16, not_null => 1 },
rate => { type => 'integer', not_null => 1 },
],
primary_key_columns => ['vat_id'],
);
sub netto2vat { ... }
sub brutto2vat { ... }
Real life VS tests
# In real lifemy $vat = VAT->new(vat_id => 'VAT_20')->load();
my $vat_amount = $vat->netto2vat(102.51);
...
# In testsmy $vat_obj =VAT->new( rate => 20 );
is( $vat_obj->brutto2vat(123.01), '20.50', 'Checking brutto2vat calculations' );
is( $vat_obj->brutto2vat(456.00), '76.00', 'Checking brutto2vat calculations' );
is( $vat_obj->netto2vat(102.51), '20.50', 'Checking netto2vat calculations' );
is( $vat_obj->netto2vat(380.00), '76.00', 'Checking netto2vat calculations' );
Real life VS tests (complex)
# In real lifemy $fin_event = FinEvent->new(
amount => 102.22,
# vat_object => ???
)->load();
my $amount = $fin_event->calculate_vat_amount();
# In testsmy $fin_event = FinEvent->new(
vat_object => VAT->new( rate=>20 ),
amount => 102.22
);
is( $fin_event->calculate_vat_amount(), 20.44, 'VAT calculation' );
But still a lot of logic requires DB
So, bad news again:
In complex operation injection is not always suitable due to complex dependencies
Using of DBI mock objects is not a good way due to blackbox nature of the ORM framework
One Data Base for whole Model
Simple solution
Just to use the same predefined set of data for all test countries, users, companies, banks, materials, products, customers, partners, vats, stocks, languages, currencies,currency rates, units... and a lot more
Shared set of predefined data worked until we started work on aggregated reports. Every report require a new set of test data which breaks other tests data.
Rule 3: Individual test environment for each tests set
Recreate database for each tests set?
=> It takes too much time :(
Use embedded DB like SQLite (you can copy file)?
=> It requires support of two database schemas :(
Manually delete data after each test?
=> It requires additional cleanup procedures :(
What we do?
Just revert transaction after test :)
use Test::More;
my $db = Rose::DB->new_or_cached();
$db->begin_work();
prepare_test_data();
do_testing();
$db->rollback();
Conclusions
Rule 1: No tests for simple classes
Rule 2: Try inject dependencies
Rule 3: Individual test environment for each test
Take a look at our testsmy $t = Test::Project->new(); # starts new transaction
$t->standard_setup()
->add_material_from_partner( 1200 )
->add_service_from_partner( 600, {date=>'2011-10-11'} )
->add_material_sale(60, {currency_rate=>800, currency_id=>'USD'})
;
iterate_test_data( 'report_a', sub {
my $data = shift;
my $report = Report::A->new(%{ $data->{input} });
$t->test_correct_report($report, $data);
});
# on $t->DESTROY() - will revert transaction
Viktor [email protected]
http://koorchik.blogspot.com
http://search.cpan.org/~koorchik/
https://github.com/koorchik
WebbyLab
http://webbylab.com