Easy Fixtures With DBIx::Class

As part of my rewriting of my testing classes (tired of hearing about it yet?), I found myself at the part where I needed to explain test fixtures. A fixture, in this context, is simply a particular known state that you can test against. In particular, I'm focusing on using database fixtures in tests. There's not a lot of good discussion about this and that's unfortunate because fixtures can really benefit your test suite.

Poorly managed fixture data

Unfortunately, many people struggle with the concept and it's easy to see why: fixtures are embedding state in your code and that's something we usually discourage. As a result, there isn't enough discussion about best practices when using fixtures and fixture code in test suites is a mess. What I frequently see is two types of issues:

  • Cutting-and-pasting fixture code all over the test suite
  • Loading a huge blob of "known" data covering all test cases you can think of (or just dump of production)

The first is obviously bad, but the second can make it hard to fine-tune a test case for a particular problem. For example, if you're trying to test the rare bug that manifests when you have several customers but none with orders, what do you do when you have a bunch of orders already in the database? Well, you delete them, right? Except that all sorts of foreign key constraints kick in and it gets hard to delete those orders.

And then there's the setup/teardown performance problem. Every test case should start with a pristine environment, but people often don't clean up after themselves and tests sometimes rely on the results of previous tests. Dropping and recreating a database is slow. Truncating and repopulating the database from a huge wad of data can be slow. How do we fix this?

Fixing the problem

Several years ago, I talked about a way of only truncating and rebuilding what you needed, which turned out to be a very fast solution, though it was technically tricky to implement. In that presentation, I actually recommended against using transactions for this and there were several reasons why, but now I think my solution was overkill. Transactions are fast and get you most of what you need. More importantly, now that parallel testing is becoming more popular, we need isolation between processes and transactions are the easiest way to do that.

So getting back to my training, I want to show attendees the concept of fixtures and not get too bogged down into detail. I figure that once they understand fixtures, they can figure out for themselves the best way to implement them. Given that, I researched fixture modules for Perl. The first thing I looked at was DBIx::Class::Fixtures and to be honest, I couldn't wrap my head around the documentation. Plus, it requires JSON files and hard-coded primary keys.

Test::Fixture::DBI requires YAML files as does Test::Fixture::DBIC::Schema. And both require hard-coded primary keys in the definitions.

My solution

Instead, I wanted it to be easy to define a DBIx::Class fixture using Perl without being tied to a serialization format. I also wanted a dead simple interface like this:

my $fixtures = My::Fixture->new(schema => $schema);
$fixtures->load(@list_of_fixture_names);
# run tests
$fixtures->unload; # or have them rolled back when out of scope

I couldn't find that, so I wrote it and you can read my first pass at a ftutorial for writing fixtures.

Let's say you have a people table that looks like this:

CREATE TABLE people (
    person_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name      VARCHAR(255) NOT NULL,
    email     VARCHAR(255)     NULL UNIQUE
);

Creating a fixture for it looks like this:

basic_person => {
    new   => 'Person',
    using => {
        name     => 'Bob',
        email    => 'not@home.com',
    },
}

And in your code:

$fixture->load('basic_person');
my $person = $schema->resultset('Person')->find({email => 'not@home.com'});

As soon as you call load for the first time, a transaction is started. You can call load multiple times to load more fixtures as you go along. Calling unload or letting the fixture object go out of scope automatically rolls back the transaction.

For the above example, what happens if the person doesn't have an email? You have to know the ID to fetch the person, so this shortcut is available:

$fixture->load('basic_person');
my $person = $fixture->get_result('basic_person');

This shortcut makes it quick and easy to fetch a DBIx::Class::ResultSource object.

So let's add a customers table. Each person might be a customer.

CREATE TABLE customers (
    customer_id    INTEGER PRIMARY KEY AUTOINCREMENT,
    person_id      INTEGER  NOT NULL UNIQUE,
    first_purchase DATETIME NOT NULL,
    FOREIGN KEY(person_id) REFERENCES people(person_id)
);

Here's how that might be defined as a fixture:

person_with_customer => {
    new   => 'Person',
    using => {
        name     => "sally",
        email    => 'person@customer.com',
    },
    next => [qw/basic_customer/],
},
basic_customer => {
    new      => 'Customer',
    using    => { first_purchase => $datetime_object },
    requires => { person_with_customer => 'person_id' },
},

The next key for the person fixture says "after you load this fixture, load the following fixtures" (in this case, only one).

The requires key for the basic customer says "before you load this fixture, load these other fixtures and populate my data based on their values". Because many people use id for the primary key name, the requires syntax can be extended like this:

requires => {
    person_with_customer => {
        our   => 'person_id',
        their => 'id',
    },
}

Because person_with_customer has a next field and basic_customer has a previous field, you can load either of these fixtures and the other fixture will be loaded.

$fixture->load('person_with_customer');
# same thing
$fixture->load('basic_customer');

Note that you can easily fetch both of those fixtures:

$fixture->load('basic_customer'); # or person_with_customer

my $person   = $fixture->get_result('person_with_customer');
my $customer = $fixture->get_result('basic_customer');

is $person->id, $customer->person->id,
  'Our customer should be attached to the right person';

If you wanted to be able to load that person without loading the associated customer, you'd simply omit the next field and load person_with_customer. Later, if you load basic_customer (assuming you haven't called $fixtures->unload or let the fixture object go out of scope), then the customer will automatically be attached to the previously loaded person.

That's actually enough to handle the bulk of what you need for fixtures. I don't tie you into a particular implementation of the fixtures and the tutorial shows you how to get started. It also shows a more complicated example of setting up a many-to-many relationship and "fixture groups".

The code is also available on github. You can read through the test suite to see various examples. Note that the code is alpha and bug reports are appreciated, as are test cases.

1 Comment

This is great and much needed! Thank you for this contribution.

Leave a comment

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you. And don't forget to buy my book! http://www.amazon.com/Beginning-Perl-Curtis-Poe/dp/1118013840/