Commit | Line | Data |
61b85627 |
1 | =head1 Testing difficult-to-test database models |
2 | |
3 | In this article we're going to describe a technique for testing database |
4 | heavy web applications using either a temporary testing database, or using |
5 | the DSN defined in your catalyst application depending on the presence of |
6 | an envoronmnet variable. If the latter we will not delete the contents of |
7 | the database afterwards because this workflow suggests that we will want to |
8 | poke around our application manually with the application in a known state. |
9 | |
10 | Basically, providing automated testing of complex databases is a pain. For |
11 | generic type functions (e.g. the development of libraries rather than |
12 | applications), mock objects (objects that mimic the interface of a real |
13 | object) are useful for unit testing. But in the running on the seat of |
14 | your pants development style that commercial web applications often |
15 | require, small changes to functionality can wreack havock with your mock |
16 | objects, and they rapidly become more trouble than they're worth. |
17 | |
18 | Which is where L<Test::DBIx::Class> comes in. The rest of this article |
19 | will explain how to achieve three different use-cases for Test::DBIx::Class |
20 | and Catalyst. |
21 | |
22 | =head2 The easy use case - Inferring the Database from the DBIC result classes. |
23 | |
24 | For a straightforward database schema where the L<DBIx::Class> (DBIC) |
25 | result classes can be used out of the box, one can simply use |
26 | L<Test::DBIx::Class> to infer the database schema into a temporary |
27 | database, using a temporary Postgresql instance via |
28 | L<Test::DBIx::Class::SchemaManager::Trait::Testpostgresql>: |
29 | |
30 | use Test::More; |
31 | |
32 | use strict; |
33 | use warnings; |
34 | use Test::WWW::Mechanize::Catalyst 'MyApp'; |
35 | |
36 | use Test::DBIx::Class { |
37 | schema_class => MyApp->model('DB')->schema_class, |
38 | traits => ['Testpostgresql'] |
39 | }, |
40 | 'User', 'Adverts'; # only create the tables for the User and Advert Result |
41 | # classes |
42 | |
43 | As an alternative to naming specific tables in the last part of the C<use |
44 | Test::DBIx::Class> declaration, you can use L<qw/:resultsets/> instead of |
45 | C<'User', 'Adverts'> in the example avove, to import all Result classes |
46 | defined in the C<schema_class>. |
47 | |
48 | The next thing to produce the appropriate L<Moose> meta-object incantation |
49 | to swap out the configured C<< MyApp->model('DB')->schema >> with the |
50 | temporary one we want to use instead (note, this works even when we start |
51 | doing the clever override things in the next two sections): |
52 | |
53 | # make TDBIC schema and app schema the same |
54 | my $db_meta = MyApp::Model::DB->meta; |
55 | $db_meta->make_mutable; |
56 | $db_meta->add_around_method_modifier( schema => sub { Schema() } ); |
57 | $db_meta->make_immutable; |
58 | |
59 | Now that we've done this we can start making requests: |
60 | |
61 | my $mech = Test::WWW::Mechanize::Catalyst->new; |
62 | $mech->get('whatever'); |
63 | ### etc |
64 | |
65 | And the database operations should all really happen, but to a temporary |
66 | database that gets deleted at the end of the run. This is especially |
67 | useful if you have lots of tests that all need a pristine copy of the |
68 | database with their own fixtures, as it means you can speed things up by |
69 | running in parallell (e.g. to run 3 tests in parallell run C< prove -l -j 3 |
70 | t >). |
71 | |
72 | =head2 OK Good. This time let's optionally override the temporary database |
73 | with the developer's DSN |
74 | |
75 | One development style which works fairly well is to write tests to run on |
76 | the development database, and then have a play around at the end of the |
77 | test run either with the perl debugger or using the built in development |
78 | server. However this means that one can't always rely on having a |
79 | temporary testing database for running tests. |
80 | |
81 | So in this case we use the application's configured database instead. Note |
82 | this requires a bit more trickery than when we're just using a temporary TDBIC |
83 | database: |
84 | |
85 | use Test::More; |
86 | |
87 | use FindBin qw/$Bin/; |
88 | use lib "$Bin/lib"; |
89 | use Test::WWW::Mechanize::Catalyst qw/MyApp/; |
90 | |
91 | BEGIN { |
92 | use MyApp; |
93 | my %tdbic_args = ( schema_class => MyApp->model('DB')->schema_class, |
94 | traits => [qw/Testpostgresql/], |
95 | ); |
96 | if ($ENV{DEV_DB}) { |
97 | %tdbic_args = ( |
98 | connect_info => |
99 | MyApp->model('DB')->schema_class->storage->connect_info, |
100 | force_drop_table => 1, |
101 | keep_db => 1, # assume we want to keep the test db at |
102 | # the end of the test run |
103 | %tdbic_args |
104 | ) |
105 | }; |
106 | |
107 | # this pattern because we're messing with instantiation in BEGIN |
108 | require Test::DBIx::Class; |
109 | Test::DBIx::Class->import(\%tdbic_args, qw/:resultsets/); |
110 | |
111 | =head2 Fine, that's the simple cases, what about the harder cases? |
112 | |
113 | In many situations it's not desirable to infer the database directly from |
114 | the DBIC schema classes. While it is possible to put all the metadata (for |
115 | example including stuff that requires custom database engine extensions) |
116 | into the DBIC schema, this is not necessarily desirable. For example if you |
117 | have a process whereby your database schemas are signed off (and likely |
118 | modified) by a DBA you're likely going to want the master copy of your |
119 | database in SQL rather than DBIC files. Likewise if you have evil business |
120 | logic that's best encapsulated in a database trigger you'll likely hit the |
121 | same type of problems. |
122 | |
123 | Given we're using a postgresql database in this instance, we need some pg |
124 | specific code to spin up either a temporary database or to repopulate the |
125 | development database. So to complement |
126 | L<Test::DBIx::Class::SchemaManager::Trait::Testpostgresql>, we've written |
127 | our own internal C<Test::DBIx::Class::SchemaManager::Trait::DeploySQL> |
128 | class that should be kept in C< |
129 | t/lib/Test/DBIx/Class/SchemaManager/Trait/DeploySQL.pm > in your app's |
130 | directory tree. It's possible this could be released as a CPAN module one |
131 | day, but at this stage we suspect that every development situation is |
132 | sufficiently different that it's probably better just to leave these |
133 | particular bits of wheel lying around for other people to adapt, |
134 | rather than offering an explicit canned solution that's supposed to work |
135 | for everybody. |
136 | |
137 | Meanwhile here's what we have for our postgresql database populated by sql statements: |
138 | |
139 | use Moose::Role; |
140 | use MyApp; |
141 | |
142 | before prepare_schema_class => sub { |
143 | my ($self, $schema_class) = @_; |
144 | { no warnings 'redefine'; |
145 | no strict 'refs'; |
146 | *{$schema_class.'::deploy'} = sub { $self->_deploy_sql(@_) }; |
147 | } |
148 | }; |
149 | |
150 | sub _deploy_sql { |
151 | my($self, $schema) = @_; |
152 | my $port = $self->postgresqlobj->port; |
153 | my $args = $self->postgresqlobj->postmaster_args; |
154 | my $storage = $schema->storage; |
155 | my $app_root = MyApp->path_to(); |
156 | my ($db_name) = $storage->connect_info->[0]->{dsn} =~ /dbname=(.*?)(;|$)/; |
157 | my ($db_user) = $storage->connect_info->[0]->{user}; |
158 | my @sql_files = qw/list of sql files here/; |
159 | my $psql_cmd; |
160 | unless ($ENV{DEV_DB}) { |
161 | $psql_cmd = "/usr/bin/psql $args -p $port"; |
162 | $storage->dbh_do(sub { |
163 | system qq{$psql_cmd -U$db_user $db_name -q -c "create language plpgsql"}}); |
164 | } |
165 | else { |
166 | $psql_cmd = "/usr/bin/psql"; |
167 | } |
168 | $storage->dbh_do(sub { |
169 | system "$psql_cmd -U$db_user $db_name -f $app_root/misc/db/$_"}) |
170 | for @sql_files; |
171 | } |
172 | |
173 | 1; |
174 | |
175 | The main thing to note here is that wrapping the C<system> calls in a |
176 | C<< $storage->dbh_do > call ensures that the database handle from DBI is |
177 | connected to the database using the failsafe mechanisms in |
178 | L<DBIx::Class::Schema::Storage>. |
179 | |
180 | So finally, deploying to our temporary database using L<Test::DBIx::Class> |
181 | and either a temporary or a development database from SQL files is done. |
182 | Now to start up the test file we change the traits in C<%tdbic_args> to |
183 | this: |
184 | |
185 | traits => [qw/Testpostgresql DeploySQL/], |
186 | |
187 | Somewhat intricate, but for complicated development situations definitely |
188 | worth it. |
189 | |
190 | =head2 TODO |
191 | |
192 | Our development team is still working out the best way to use this system, |
193 | but so far it's been really very handy indeed. What would be good next is |
194 | to work out how to modularise a lot of the boilerplate above so it can be |
195 | C<use>d or C<require>d as a single line in each test file. We'll get there |
196 | soon. |
197 | |
198 | =head3 AUTHORS AND COPYRIGHT |
199 | |
200 | Words and a little bit of code: |
201 | Kieren Diment <zarquon@cpan.org> |
202 | |
203 | Most of the code: |
204 | Eden Cardim <edencardim@gmail.com> |
205 | |
206 | =head3 LICENCE |
207 | |
208 | This documentation can be redistributed it and/or modified under the same terms as Perl itself. |