Commit | Line | Data |
61b85627 |
1 | =head1 Testing difficult-to-test database models |
2 | |
b9e2809b |
3 | In this article we're going to describe a technique for testing database-heavy |
4 | web applications using either a temporary testing database, or using the DSN |
5 | defined in your Catalyst application depending on the presence of an |
6 | environment variable. If the latter, we will not delete the contents of the |
7 | database afterwards, because this workflow suggests that we will want to poke |
8 | around our application manually with the application in a known state. |
9 | |
10 | Basically, providing automated testing of complex databases is a pain. For |
61b85627 |
11 | generic type functions (e.g. the development of libraries rather than |
12 | applications), mock objects (objects that mimic the interface of a real |
b9e2809b |
13 | object) are useful for unit testing. But in the running on the |
14 | seat-of-your-pants development style that commercial web applications often |
15 | require, small changes to functionality can wreak havoc with your mock |
61b85627 |
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'); |
b9e2809b |
63 | ### etc. |
61b85627 |
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 |
b9e2809b |
69 | running in parallel (e.g. to run 3 tests in parallel run C< prove -l -j 3 |
61b85627 |
70 | t >). |
71 | |
b9e2809b |
72 | =head2 OK Good. This time let's optionally override the temporary database |
61b85627 |
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 |
b9e2809b |
77 | test run either with the Perl debugger or using the built in development |
61b85627 |
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 |
b9e2809b |
120 | logic that's best encapsulated in a database trigger, you'll likely hit the |
61b85627 |
121 | same type of problems. |
122 | |
b9e2809b |
123 | Given we're using a Postgres database in this instance, we need some |
124 | Pg-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 our |
127 | own internal C<Test::DBIx::Class::SchemaManager::Trait::DeploySQL> class that |
128 | should be kept in C< t/lib/Test/DBIx/Class/SchemaManager/Trait/DeploySQL.pm > |
129 | in your app's directory tree. It's possible this could be released as a CPAN |
130 | module one day, but at this stage we suspect that every development situation |
131 | is sufficiently different that it's probably better just to leave these |
132 | particular bits of wheel lying around for other people to adapt, rather than |
133 | offering an explicit canned solution that's supposed to work for everybody. |
134 | |
135 | Meanwhile here's what we have for our Postgres database populated by SQL statements: |
61b85627 |
136 | |
137 | use Moose::Role; |
138 | use MyApp; |
139 | |
140 | before prepare_schema_class => sub { |
141 | my ($self, $schema_class) = @_; |
142 | { no warnings 'redefine'; |
143 | no strict 'refs'; |
144 | *{$schema_class.'::deploy'} = sub { $self->_deploy_sql(@_) }; |
145 | } |
146 | }; |
147 | |
148 | sub _deploy_sql { |
149 | my($self, $schema) = @_; |
150 | my $port = $self->postgresqlobj->port; |
151 | my $args = $self->postgresqlobj->postmaster_args; |
152 | my $storage = $schema->storage; |
153 | my $app_root = MyApp->path_to(); |
154 | my ($db_name) = $storage->connect_info->[0]->{dsn} =~ /dbname=(.*?)(;|$)/; |
155 | my ($db_user) = $storage->connect_info->[0]->{user}; |
156 | my @sql_files = qw/list of sql files here/; |
157 | my $psql_cmd; |
158 | unless ($ENV{DEV_DB}) { |
159 | $psql_cmd = "/usr/bin/psql $args -p $port"; |
160 | $storage->dbh_do(sub { |
161 | system qq{$psql_cmd -U$db_user $db_name -q -c "create language plpgsql"}}); |
162 | } |
163 | else { |
164 | $psql_cmd = "/usr/bin/psql"; |
165 | } |
166 | $storage->dbh_do(sub { |
167 | system "$psql_cmd -U$db_user $db_name -f $app_root/misc/db/$_"}) |
168 | for @sql_files; |
169 | } |
170 | |
171 | 1; |
172 | |
173 | The main thing to note here is that wrapping the C<system> calls in a |
174 | C<< $storage->dbh_do > call ensures that the database handle from DBI is |
175 | connected to the database using the failsafe mechanisms in |
176 | L<DBIx::Class::Schema::Storage>. |
177 | |
178 | So finally, deploying to our temporary database using L<Test::DBIx::Class> |
179 | and either a temporary or a development database from SQL files is done. |
180 | Now to start up the test file we change the traits in C<%tdbic_args> to |
181 | this: |
182 | |
183 | traits => [qw/Testpostgresql DeploySQL/], |
184 | |
185 | Somewhat intricate, but for complicated development situations definitely |
186 | worth it. |
187 | |
188 | =head2 TODO |
189 | |
190 | Our development team is still working out the best way to use this system, |
191 | but so far it's been really very handy indeed. What would be good next is |
192 | to work out how to modularise a lot of the boilerplate above so it can be |
193 | C<use>d or C<require>d as a single line in each test file. We'll get there |
194 | soon. |
195 | |
196 | =head3 AUTHORS AND COPYRIGHT |
197 | |
198 | Words and a little bit of code: |
199 | Kieren Diment <zarquon@cpan.org> |
200 | |
201 | Most of the code: |
202 | Eden Cardim <edencardim@gmail.com> |
203 | |
204 | =head3 LICENCE |
205 | |
206 | This documentation can be redistributed it and/or modified under the same terms as Perl itself. |