Testing DBIx::Class Models without the Database

| 4 Comments

DBIx::Class is the first ORM I've found which provides more benefits than headaches. It's not perfect—it has a learning curve—but its power and flexibility have simplified several of my projects.

One of my few ambivalences toward DBIC is that it commingles database operations (create, retrieve, update, and destroy) with model-specific operations ("level up this character", "sell the magic sword", "deactivate the smoke trap"). This is useful in that my objects have smart persistence and updating and relationship tracking provided by their DBICness, but it's difficult in that sometimes I want a stricter separation of database concerns from data model concerns.

Suppose I want to test that a document processing model can successfully filter out many hundreds of thousands of algorithmically generated permutations of unwanted data. Suppose, as is the case, that these documents are DBIC objects normally retrieved from a database. A test database might seem like just the thing for data-driven testing, but in this case it seems more work to generate a large transient database full of hundreds of thousands of documents to satisfy the constraints of DBIC.

In other words, I want a way to generate a new model object programmatically without having to store it in and retrieve it from a database. (Alex Hartmaier and Matt S. Trout both reminded me of the $rs->new_result() method which creates an object given the appropriate resultset but does not store it in the database.)

I do have tests which verify that the storage concerns behave as appropriate. They represent a small investment in mock data which sufficiently exercises all of the cases my code needs to handle. My other test concerns have little to do with the database itself. They care about what the model objects do with their data, not how they get that data.

Thank goodness DBIC is compatible with Moose.

I extracted all of the non-database model methods into a role. That role requires the accessors the database model provides for persistent data. I created a very simple dummy class which has those necessary attributes and performs that role. Then I wrote a very, very simple loader function which generates the necessary data algorithmically, instantiates instances of the dummy class, and tests the role's methods.

(I plan to write a longer article for Perl.com showing example code.)

The result is a model class which consists solely of the code generated from DBIx::Class::Schema::Loader and a with statement to apply a couple of roles. The tests are in two parts: one tests the model-specific code. The other tests the persistence-specific code, itself a combination of the generated code and another role which collects the remaining persistence behavior.

Even though both roles have an obvious coupling in terms of providing necessary behavior to the model (and to each other), decoupling them in terms of storage provides much improved testability—and, I suspect, more opportunities for reuse and genericity throughout the system.

While explanations of the value of Perl roles often focus on reusability and the separation of similar concerns from otherwise unrelated classes, roles can also provide a separation between dissimilar behaviors and concerns. In this case, it doesn't matter to the role methods where the data comes from (a live database, a testing database, an algorithm, hard-coded in a test case, the command line, wherever). It only matters that that data is there.

This technique doesn't always work. This technique isn't always appropriate. This technique does not replace verification that your behavior roles interoperate with your persistence models appropriately. Even so, it has simplified a lot of my code and improved my tests greatly.

4 Comments

"The result is a model class which consists solely of the code generated from DBIx::Class::Schema::Loader and a with statement to apply a couple of roles. "

Just FYI, in case you find it useful: I recently added 'result_roles_map' and 'result_roles' options to dbic::Schema::Loader. They allow you to add roles to your generated Result classes.

I don't think it's on CPAN yet, but it's in the master branch in git now.

"This is useful in that my objects have smart persistence and updating and relationship tracking provided by their DBICness, but it's difficult in that sometimes I want a stricter separation of database concerns from data model concerns."

If you create a Character class that includes (is composed of?) a Result::Character DBIC class, or a class that implements a DBICish role, then your domain object is decoupled from your database, at the expense of some adaptor/wrapper code for attribute getters/setters.

I just wanted to also add: I actually do the same thing chromatic talks about (hense the addition of result_roles_map options to dbic::s::l).

And so far it's been a great success.

I have ...::Role::Result::Extend::[TableName] roles that do any row-level business logic for that table. They simply require the needed fields from the result class.
Any roles relevant to multiple tables can go below the 'Extend' namespace. e.g. ...::Role::Result::Keywords

This has 2 big advantages:
1) As chromatic illustrated - swapping out the source of the data
2) Ability to delete and regenerate schemas when you get merge conflicts due to checksums. i.e. no custom code exists in those Result classes.

would DBD::Mock not work for your case? https://metacpan.org/module/DBD::Mock or some problem with it I haven't thought of?

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on May 18, 2011 5:55 PM.

Decoupling, Testability, and Synthetic Attributes was the previous entry in this blog.

Show It Off is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?