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