Using the fixture module

< fixture project

< farmdev.com

Contents

Download / Install

Using the easy_install command:

easy_install fixture

Or, if you want to create a link to the source without installing anything, cd into the root directory and type:

python setup.py develop

Or ... if you're old school, this works with or without setuptools:

python setup.py install

Note

The above commands may require root access

Loading and referencing test data

There are a couple ways to test a database-backed application. You can create mock objects and concentrate entirely on unit testing individual components without testing the database layer itself, or you can simply load up sample data before you run a test. Thanks to sqlite in-memory connections (a special DSN, sqlite:///:memory:), the latter may be more efficient than you think. What's more important is that when you create a functional test (one that proves the interface to a feature works as expected), testing with real data can help you achieve better code coverage.

But it's easy enough to insert data line by line in a setup function, right? Or simply load a SQL file? Yes, but this has two major downsides: you often have to worry about and manage complex chains of foreign keys manually; and when referencing data values later on, you either have to copy/paste the values or pass around lots of variables.

The fixture module simplifies this by focusing on two independent components:

DataSet
defines sets of sample data
Fixture
knows how to load data

The details of loading the actual data is left up to your application's database layer itself (more info on this later).

Using DataSet

Before loading data, you need to define it. A single subclass of DataSet represents a database relation in Python code. Think of the class as a table, each inner class as a row, and each attribute per row as a column value. For example:

>>> from fixture import DataSet
>>> class Authors(DataSet):
...     class frank_herbert:
...         first_name = "Frank"
...         last_name = "Herbert"

The inner class frank_herbert defines a row with the columns first_name and last_name. The name frank_herbert is an identifier that you can use later on, when you want to refer to this specific row.

The main goal will be to load this data into something useful, like a database. But notice that the id values aren't defined in the DataSet. This is because the database will most likely create an id for you when you insert the row (however, if you need to specify a specific id number, you are free to do so). How you create a DataSet will be influenced by how the underlying data object saves data.

Inheriting DataSet rows

Since a row is just a Python class, you can inherit from a row to morph its values, i.e.:

>>> class Authors(DataSet):
...     class frank_herbert:
...         first_name = "Frank"
...         last_name = "Herbert"
...     class brian_herbert(frank_herbert):
...         first_name = "Brian"

This is useful for adhering to the DRY principle (Don't Repeat Yourself) as well as for testing edge cases.

Note

The primary key value will not be inherited from a row. See Customizing a DataSet if you need to set the name of a DataSet's primary key to something other than id.

Referencing foreign DataSet classes

When defining rows in a DataSet that reference foreign keys, you need to mimic how your data object wants to save such a reference. If your data object wants to save foreign keys as objects (not ID numbers) then you can simply reference another row in a DataSet as if it were an object.:

>>> class Books(DataSet):
...     class dune:
...         title = "Dune"
...         author = Authors.frank_herbert
...     class sudanna:
...         title = "Sudanna Sudanna"
...         author = Authors.brian_herbert

During data loading, the reference to DataSet Authors.brian_herbert will be replaced with the actual stored object used to load that row into the database. This will work as expected for one-to-many relationships, i.e.:

>>> class Books(DataSet):
...     class two_worlds:
...         title = "Man of Two Worlds"
...         authors = [Authors.frank_herbert, Authors.brian_herbert]

However, in some cases you may need to reference an attribute that does not have a value until it is loaded, like a serial ID column. (Note that this is not supported by the sqlalchemy data layer when using sessions.) To facilitate this, each inner class of a DataSet gets decorated with a special method, ref(), that can be used to reference a column value before it exists, i.e.:

>>> class Books(DataSet):
...     class dune:
...         title = "Dune"
...         author_id = Authors.frank_herbert.ref('id')
...     class sudanna:
...         title = "Sudanna Sudanna"
...         author_id = Authors.brian_herbert.ref('id')

This sets the author_id to the id of another row in Author, as if it were a foreign key. But notice that the id attribute wasn't explicitly defined by the Authors data set. When the id attribute is accessed later on, its value is fetched from the actual row inserted.

Customizing a Dataset

A DataSet can be customized by defining a special inner class named Meta. See the DataSet.Meta API for more info.

Using LoadableFixture

A DataSet class is loaded via some storage medium, say, an object that implements a Data Mapper or Active Record pattern. A Fixture is an environment that knows how to load data using the right objects. Behind the scenes, the rows and columns of the DataSet are simply passed off to the storage medium so that it can save the data.

Supported storage media

The Fixture class is designed to support many different types of storage media and there is a section later about creating your own Fixture. Here are the various storage media supported by built-in Fixture subclasses:

SQLAlchemy

DataSet classes can be loaded into Table objects or mapped classes via the sqlalchemy module:

>>> from fixture import SQLAlchemyFixture

>>> from sqlalchemy import create_session
>>> from sqlalchemy.ext.sessioncontext import SessionContext
>>> from fixture.examples.db import sqlalchemy_examples
>>> dbfixture = SQLAlchemyFixture(
...                 session_context=SessionContext(create_session),
...                 env=sqlalchemy_examples)
...

For the more documentation see SQLAlchemyFixture API

Elixir

DataSet class can be loaded into Elixir entities by using the SQLAlchemyFixture (see previous example).

SQLObject

DataSet classes can be loaded into SQLObject classes via the sqlobject module:

>>> from fixture import SQLObjectFixture

>>> from fixture.examples.db import sqlobject_examples
>>> dbfixture = SQLObjectFixture(
...     dsn="sqlite:/:memory:", env=sqlobject_examples)
...

For the more documentation see SQLObjectFixture API.

An Example Loading Data Using SQLAlchemy

Fixture is designed for applications that already define a way of accessing its data; the LoadableFixture just "hooks in" to that interface. To start this example, here is some sqlalchemy code to set up a database of books and authors:

>>> from sqlalchemy import *
>>> engine = create_engine('sqlite:///:memory:')
>>> meta = BoundMetaData(engine)
>>> session = create_session(engine)
>>> authors = Table('authors', meta,
...     Column('id', Integer, primary_key=True),
...     Column('first_name', String),
...     Column('last_name', String))
...
>>> class Author(object):
...     pass
...
>>> mapper(Author, authors) #doctest: +ELLIPSIS
<sqlalchemy.orm.mapper.Mapper object at ...>
>>> books = Table('books', meta,
...     Column('id', Integer, primary_key=True),
...     Column('title', String),
...     Column('author_id', Integer, ForeignKey('authors.id')))
...
>>> class Book(object):
...     pass
...
>>> mapper(Book, books, properties={
...     'author': relation(Author, backref='books')
... }) #doctest: +ELLIPSIS
<sqlalchemy.orm.mapper.Mapper object at ...>
>>> meta.create_all()

Consult the sqlalchemy documentation for further examples of data mapping.

Defining a Fixture

This is a fixture with minimal configuration to support loading data into the Book or Author mapped classes:

>>> from fixture import SQLAlchemyFixture
>>> dbfixture = SQLAlchemyFixture(
...     env={'BookData': Book, 'AuthorData': Author},
...     session=session )
...

There are several shortcuts, like fixture.style.NamedDataStyle and specifying the session_context keyword.

Note

  • Any keyword attribute of a LoadableFixture can be set later on as an attribute of the instance.
  • LoadableFixture instances can safely be module-level objects
  • An env can be a dict or a module

Loading DataSet objects

The job of the Fixture object is to load and unload DataSet objects. Let's consider the following DataSet objects (reusing the examples from earlier):

>>> from fixture import DataSet
>>> class AuthorData(DataSet):
...     class frank_herbert:
...         first_name = "Frank"
...         last_name = "Herbert"
>>> class BookData(DataSet):
...     class dune:
...         title = "Dune"
...         author = AuthorData.frank_herbert

As you recall, we passed a dictionary into the Fixture that associates DataSet names with storage objects. Using this dict, a Fixture.Data instance now knows to use the sqlalchemy mapped class Book when saving a DataSet named BookData. Since we also gave it a session keyword, this will be used to actually save objects:

>>> data = dbfixture.data(AuthorData, BookData)
>>> data.setup()
>>> all_books = list(session.query(Book).select())
>>> all_books #doctest: +ELLIPSIS
[<...Book object at ...>]
>>> all_books[0].author.first_name
'Frank'
>>> data.teardown()
>>> list(session.query(Book).select())
[]

Discovering storable objects with Style

If you didn't want to create a strict mapping of DataSet class names to their storable object names you can use Style objects to translate DataSet class names. For example, consider this Fixture :

>>> from fixture import SQLAlchemyFixture
>>> from fixture.style import TrimmedNameStyle
>>> dbfixture = SQLAlchemyFixture(
...     env=globals(),
...     style=TrimmedNameStyle(suffix="Data"),
...     session=session )
...

This would take the name AuthorData and trim off "Data" from its name to find Author, its mapped sqlalchemy class for storing data. Since this is a logical convention to follow for naming DataSet classes, you can use a shortcut:

>>> from fixture.style import NamedDataStyle
>>> dbfixture = SQLAlchemyFixture(
...     env=globals(),
...     style=NamedDataStyle(),
...     session=session )
...

See the Style API for all available Style objects.

Loading DataSet classes in a test

Now that you have a Fixture object to load DataSet classes you are ready to write some tests. You can either write your own code that creates a data instance and calls setup/teardown manually (like in previous examples), or you can use one of several utilities.

As a hoky attempt to make these tests somewhat realistic, here is a function we will be testing, that returns True if a book by author or title is in stock:

>>> def in_stock(book_title=None, author_last_name=None):
...     if book_title:
...         rs = session.query(Book).select(books.c.title==book_title)
...     elif author_last_name:
...         rs = session.query(Book).select(
...                 authors.c.last_name==author_last_name,
...                 from_obj=[books.join(authors)])
...     else:
...         return False
...     if len(list(rs)):
...         return True

Loading objects using DataTestCase

DataTestCase is a mixin class to use with Python's built-in unittest.TestCase:

>>> import unittest
>>> from fixture import DataTestCase
>>> class TestBookShop(DataTestCase, unittest.TestCase):
...     fixture = dbfixture
...     datasets = [BookData]
...
...     def test_books_are_in_stock(self):
...         assert in_stock(book_title=self.data.BookData.dune.title)
...
>>> suite = unittest.TestLoader().loadTestsFromTestCase(TestBookShop)
>>> unittest.TextTestRunner().run(suite)
<unittest._TextTestResult run=1 errors=0 failures=0>

Re-using what was created earlier, the fixture attribute is set to the Fixture instance and the datasets attribute is set to a list of DataSet classes. When in the test method itself, as you can see, you can reference loaded data through self.data, an instance of SuperSet. Keep in mind that if you need to override either setUp() or tearDown() then you'll have to call the super methods.

See the DataTestCase API for a full explanation of how it can be configured.

Loading objects using @dbfixture.with_data

If you use nose, a test runner for Python, then you may be familiar with its discovery of test methods. Test methods (as opposed to unittest.TestCase classes) provide a quick way to write procedural tests and often illustrate concisely what features are being tested. Nose supports test methods that are decorated with setup and teardown methods and fixture provides a way to setup/teardown DataSet objects for a test method. If you don't have nose installed, simply install fixture like so and nose will be installed for you:

easy_install fixture[decorators]

The special decorator method is an instance method of a Fixture class, with_data; it can be used like so:

>>> @dbfixture.with_data(AuthorData, BookData)
... def test_books_are_in_stock(data):
...     assert in_stock(book_title=data.BookData.dune.title)
...
>>> import nose
>>> case = nose.case.FunctionTestCase(test_books_are_in_stock)
>>> unittest.TextTestRunner().run(case)
<unittest._TextTestResult run=1 errors=0 failures=0>

Like in the previous example, the data attribute is a SuperSet object you can use to reference loaded data. This is passed to your decorated test method as its first argument. (nose will run the above code automatically; the inline execution of the test here is merely for example.)

See the Fixture.Data.with_data API for more information.

Loading objects using the with statement

In Python 2.5 or later you can write test code in a more logical manner by using the with statement. Anywhere in your code, when you enter a with block using a Fixture.Data instance, the data is loaded and you have an instance in which to reference the data. When you exit, the data is torn down for you, regardless of whether there was an exception or not. For example:

from __future__ import with_statement
with dbfixture.data(AuthorData, BookData) as data:
    assert in_stock(book_title=data.BookData.dune.title)

Defining a custom LoadableFixture

It's possible to create your own LoadableFixture if you need to load data with something other than SQLAlchemy or SQLObject.

You'll need to subclass at least fixture.loadable.loadable:LoadableFixture, possibly even fixture.loadable.loadable:EnvLoadableFixture or the more useful fixture.loadable.loadable:DBLoadableFixture. Here is a simple example for creating a fixture that hooks into some kind of database-centric loading mechanism:

>>> loaded_items = set()
>>> class Author(object):
...     '''This would be your actual storage object, i.e. data mapper.
...        For the sake of brevity, you'll have to imagine that it knows
...        how to somehow store "author" data.'''
...
...     name = None # gets set by the data set
...
...     def save(self):
...         '''just one example of how to save your object.
...            there is no signature guideline for how this object
...            should save itself (see the adapter below).'''
...         loaded_items.add(self)
...     def __repr__(self):
...         return "<%s name=%s>" % (self.__class__.__name__, self.name)
...
>>> from fixture.loadable import DBLoadableFixture
>>> class MyFixture(DBLoadableFixture):
...     '''This is the class you will instantiate, the one that knows how to
...        load datasets'''
...
...     class Medium(DBLoadableFixture.Medium):
...         '''This is an object that adapts a Fixture storage medium
...            to the actual storage medium.'''
...
...         def clear(self, obj):
...             '''where you need to expunge the obj'''
...             loaded_items.remove(obj)
...
...         def visit_loader(self, loader):
...             '''a chance to reference any attributes from the loader.
...                this is called before save().'''
...
...         def save(self, row, column_vals):
...             '''save data into your object using the provided
...                fixture.dataset.DataRow instance'''
...             # instantiate your real object class (Author), which was set
...             # in __init__ to self.medium ...
...             obj = self.medium()
...             for c, val in column_vals:
...                 # column values become object attributes...
...                 setattr(obj, c, val)
...             obj.save()
...             # be sure to return the object:
...             return obj
...
...     def create_transaction(self):
...         '''a chance to create a transaction.
...            two separate transactions are used: one during loading
...            and another during unloading.'''
...         class DummyTransaction(object):
...             def begin(self):
...                 pass
...             def commit(self):
...                 pass
...             def rollback(self):
...                 pass
...         t = DummyTransaction()
...         t.begin() # you must call begin yourself, if necessary
...         return t

Now let's load some data into the custom Fixture using a simple env mapping:

>>> from fixture import DataSet
>>> class AuthorData(DataSet):
...     class frank_herbert:
...         name="Frank Herbert"
...
>>> fixture = MyFixture(env={'AuthorData': Author})
>>> data = fixture.data(AuthorData)
>>> data.setup()
>>> loaded_items
set([<Author name=Frank Herbert>])
>>> data.teardown()
>>> loaded_items
set([])

Using the fixture command

There are several issues you may run into while working with fixtures:

  1. The data model of a program is usually an implementation detail. It's bad practice to "know" about implementation details in tests because it means you have to update your tests when those details change; you should only have to update your tests when an interface changes.
  2. Data accumulates very fast and there is already a useful tool for slicing and dicing data: the database! Hand-coding DataSet classes is not always the way to go.
  3. When regression testing or when trying to reproduce a bug, you may want to grab a "snapshot" of the existing data.

fixture is a shell command to address these and other issues. It gets installed along with this module. Specifically, the fixture command accepts a path to a single object and queries that object using the command options. The output is python code that you can use in a test to reload the data retrieved by the query.

Usage

$ fixture --help
usage: fixture [options] object_path

Using the object specified in the path, generate DataSet classes (code) to
reproduce its data.  An object_path can be a python path or a file path
or anything else that a handler can recognize.

options:
  -h, --help            show this help message and exit
  --dsn=DSN             sets db connection for a handler that uses a db
  -w WHERE, --where=WHERE
                        SQL where clause, i.e. "id = 1705"
  --suffix=SUFFIX       string suffix for all dataset class names (default: Data; i.e. an Employee object becomes EmployeeData)
  --prefix=PREFIX       string prefix for all dataset class names (default: None)
  --env=ENV             module path to use as an environment for finding objects.  declaring multiple --env values will be recognized
  --require-egg=REQUIRED_EGGS
                        a requirement string to enable importing from a module that was installed in multi-version mode by setuptools.  I.E. foo==1.0.  You can repeat
                        this option as many times as necessary.
  --template=TEMPLATE   template to use; choices: ('fixture', 'testtools'), default: 'fixture'

An example

Let's set up a database and insert some data (using sqlalchemy code) so we can run the fixture command:

>>> from sqlalchemy import *
>>> DSN = 'sqlite:////tmp/fixture_example.db'
>>> from fixture.examples.db.sqlalchemy_examples import (
...                                 Author, Book, dynamic_meta)
>>> dynamic_meta.connect(DSN)
>>> dynamic_meta.create_all()
>>> session = create_session()
>>> frank = Author()
>>> frank.first_name = "Frank"
>>> frank.last_name = "Herbert"
>>> session.save(frank)
>>> dune = Book()
>>> dune.title = "Dune"
>>> dune.author = frank
>>> session.save(dune)
>>> session.flush()

It's now possible to run a command that points at our Book object, sends it a SQL query with a custom where clause, and turns the record sets into DataSet classes:

$ fixture --dsn=sqlite:////tmp/fixture_example.db --where="title='Dune'" fixture.examples.db.sqlalchemy_examples.Book
import datetime
from fixture import DataSet
from fixture.dataset import MergedSuperSet
from fixture.style import NamedDataStyle
from fixture import SQLAlchemyFixture
from fixture.examples.db.sqlalchemy_examples import authors
from fixture.examples.db.sqlalchemy_examples import books

fixture = SQLAlchemyFixture(
            env = globals(),
            style = NamedDataStyle(),
            dataclass = MergedSuperSet)


class authorsData(DataSet):
    class authors_1:
        first_name = u'Frank'
        last_name = u'Herbert'
        id = 1

class booksData(DataSet):
    class books_1:
        author_id = authorsData.authors_1.ref('id')
        id = 1
        title = u'Dune'

Notice that we only queried the Book object but we got back all the necessary foreign keys that were needed to reproduce the data (in this case, the Author data).

Creating a custom data handler

No documentation yet

Testing with a temporary file system

TempIO: a temporary file system object

This object is useful for tests that need to set up a directory structure and work with files and paths. Once you instantiate it, you have a temporary directory that will self-destruct when it falls out of scope:

>>> from fixture import TempIO
>>> tmp = TempIO()
>>> tmp #doctest: +ELLIPSIS
'/.../tmp_fixture...'

Add sub-directories by simply assigning an attribute the basename of the new subdirectory, like so:

>>> tmp.incoming = "incoming"
>>> tmp.incoming.exists()
True

The new attribute is now an absolute path to a subdirectory, "incoming", of the tmp root, as well as an object itself. Note that tmp and tmp.incoming are just string objects, but with several os.path methods mixed in for convenience. See the DirPath API for details. However, you can pass it to other objects and it will represent itself as its absolute path.

You can also insert files to the directory with putfile():

>>> foopath = tmp.incoming.putfile("foo.txt", "contents of foo")
>>> tmp.incoming.join("foo.txt").exists()
True

The directory root will self-destruct when it goes out of scope or atexit. You can explicitly delete the object at your test's teardown if you like:

>>> tmpdir = str(tmp) # making sure it's a copy
>>> del tmp
>>> os.path.exists(tmpdir)
False

API Documentation

See API documentation for detailed documentation of individual fixture components

Where to submit issues, patches, bugs

Please submit any issues, patches, failing tests, and/or bugs using the Issue Tracker on the fixture project site. Even vague ideas for improvement are welcome. If your code is used, your contribution will be documented.