3. Good news
ORM is about objects and classes
We can do testing without object
persistency
4. Rule 1: No tests for simple classes
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.
5. Rule 2: Try inject dependencies
package 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 { ... }
6. Real life VS tests
# In real life
my $vat = VAT->new(vat_id => 'VAT_20')->load();
my $vat_amount = $vat->netto2vat(102.51);
...
# In tests
my $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' );
7. Real life VS tests (complex)
# In real life
my $fin_event = FinEvent->new(
amount => 102.22,
# vat_object => ???
)->load();
my $amount = $fin_event->calculate_vat_amount();
# In tests
my $fin_event = FinEvent->new(
vat_object => VAT->new( rate=>20 ),
amount => 102.22
);
is( $fin_event->calculate_vat_amount(), 20.44, 'VAT calculation' );
8. 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
9. 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.
10. 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 :(
11. 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();
12. Conclusions
Rule 1: No tests for simple classes
Rule 2: Try inject dependencies
Rule 3: Individual test environment for each test
13. Take a look at our tests
my $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
14. Viktor Turskyi
viktor@webbylab.com
http://koorchik.blogspot.com
http://search.cpan.org/~koorchik/
https://github.com/koorchik
WebbyLab
http://webbylab.com