This explains how to use fixture in the test suite of a simple Address Book application written in Pylons powered by two tables in a SQLite database via SQLAlchemy. If you’re not already familiar with Using DataSet and Using LoadableFixture then you’ll be OK but it wouldn’t hurt to read those docs first. The concepts here will probably also work with similar Python frameworks backed by SQLAlchemy. If you’ve got something working in another framework, please let me know.
(This tutorial was written with Python 2.5.2, fixture 1.3, Pylons 0.9.7, and SQLAlchemy 0.4.8 but may work with other versions.)
First, install Pylons and create a new app as described in Getting Started. This will be an Address Book application so run the command as:
$ paster create -t pylons addressbook
Follow the prompts to use SQLAlchemy as the backend.
To work with the database you need to define a data model. Place the following code just below the init_model() to define the SQLAlchemy tables and mappers for the Address Book data. The complete module should look like:
"""The application's model objects"""
import sqlalchemy as sa
from sqlalchemy import orm
from addressbook.model import meta
def init_model(engine):
"""Call me before using any of the tables or classes in the model"""
meta.Session.configure(bind=engine)
meta.engine = engine
t_people = sa.Table('people', meta.metadata,
sa.Column('id', sa.types.Integer, primary_key=True),
sa.Column('name', sa.types.String(100)),
sa.Column('email', sa.types.String(100))
)
t_addresses_people = sa.Table('addresses_people', meta.metadata,
sa.Column('id', sa.types.Integer, primary_key=True),
sa.Column('person_id', sa.types.Integer, sa.ForeignKey('people.id')),
sa.Column('address_id', sa.types.Integer, sa.ForeignKey('addresses.id'))
)
t_addresses = sa.Table('addresses', meta.metadata,
sa.Column('id', sa.types.Integer, primary_key=True),
sa.Column('address', sa.types.String(100))
)
class Person(object):
pass
class Address(object):
pass
orm.mapper(Address, t_addresses)
orm.mapper(Person, t_people, properties = {
'my_addresses' : orm.relation(Address, secondary = t_addresses_people),
})
Take note that by default Pylons sets your sqlalchemy database to sqlite:
[app:main]
# ...
sqlalchemy.url = sqlite:///%(here)s/development.db
Note
For reference, all code shown here is available from the fixture code repository in fixture/examples/pylons_example/addressbook.
Create a book controller to show a simple list of addresses:
$ cd /path/to/addressbook
$ paster controller book
This makes the files addressbook/controllers/book.py and addressbook/tests/functional/test_book.py. Edit routing.py to set it as the default page:
# CUSTOM ROUTES HERE
map.connect('/', controller='book', action='index')
(To avoid conflicts with the default page also be sure to remove addressbook/public/index.html.)
Edit addressbook/controllers/book.py to select some addresses from the database and render a template instead of returning “Hello World”:
import logging
from pylons import request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect_to
from addressbook.lib.base import BaseController, render
from addressbook.model import meta, Person
log = logging.getLogger(__name__)
class BookController(BaseController):
def index(self):
# c, imported from addressbook/lib/base.py, is automatically
# available in your template
c.persons = meta.Session.query(Person).join('my_addresses')
return render("/book.mako")
Add the template file as addressbook/templates/book.mako and write some Python code (via Mako) to show some addresses:
<h2>
Address Book
</h2>
% for person in c.persons:
<h3>${person.name}</h3>
<h4>${person.email}</h4>
% for address in person.my_addresses:
<h4>${address.address}</h4>
% endfor
% endfor
You now have a page that lists addresses but you don’t have any address data. Fixture provides an easy way to add data to your models for automated or exploratory testing. Define the following code in a new module at addressbook/datasets/__init__.py using a naming scheme where each DataSet subclass is camel case, named after a mapped class in the model but ending in Data (read more about styles here):
from fixture import DataSet
class AddressData(DataSet):
class joe_in_montego:
address = "111 St. James St, Montego Bay, Jamaica"
class joe_in_ny:
address = "111 S. 2nd Ave, New York, NY"
class PersonData(DataSet):
class joe_gibbs:
name = "Joe Gibbs"
email = "joe@joegibbs.com"
my_addresses = [
AddressData.joe_in_montego,
AddressData.joe_in_ny]
This sets up one row to be inserted into the people table and two rows to be inserted into the addresses / addresses_people tables, declaring two addresses for our man Joe Gibbs. See Using DataSet for the details about these classes.
Notice that the DataSet classes mirror the properties we defined above for the mappers. This is because Fixture applies the DataSets to the mapped classes Address and Person respectively to save the data.
If you want to fire up the dev server and start using this data, you just need to place a few lines of code in addressbook/websetup.py, a Pylons convention for hooking into the paster setup-app devlopment.ini command.
The full code for creating tables and inserting data looks like this in addressbook/websetup.py:
"""Setup the addressbook application"""
import logging
from addressbook.config.environment import load_environment
from addressbook.model import meta
from addressbook import model
from fixture import SQLAlchemyFixture
from fixture.style import NamedDataStyle
from addressbook.datasets import PersonData
log = logging.getLogger(__name__)
def setup_app(command, conf, vars):
"""Place any commands to setup addressbook here"""
load_environment(conf.global_conf, conf.local_conf)
log.info("Creating tables")
# Create the tables if they don't already exist
meta.metadata.create_all(bind=meta.engine)
log.info("Successfully setup")
# load some initial data during setup-app :
db = SQLAlchemyFixture(
env=model, style=NamedDataStyle(),
engine=meta.engine)
data = db.data(PersonData)
log.info("Inserting initial data")
data.setup()
log.info("Done")
This will allow you to get started on your Address Book application by running:
$ cd /path/to/addressbook
$ paster setup-app development.ini
Now, start the development server:
paster serve --reload development.ini
And load up http://127.0.0.1:5000 in your browser. You should see a rendering of:
<h2>
Address Book
</h2>
<h3>Joe Gibbs</h3>
<h4>joe@joegibbs.com</h4>
<h4>111 St. James St, Montego Bay, Jamaica</h4>
<h4>111 S. 2nd Ave, New York, NY</h4>
Cool! But what you really wanted was to write some automated tests, right? Fixture makes that just as easy. You can read more about Unit Testing Pylons Apps but as of right now you should already have the file addressbook/tests/functional/test_book.py, ready and waiting for some test code.
Before running any tests you need to configure the test suite to make a database connection and create tables when the tests start. First, edit test.ini to tell your app to use a different database file so as not to disturb your development environment:
[app:main]
use = config:development.ini
# Add additional test specific configuration options as necessary.
sqlalchemy.url = sqlite:///%(here)s/tests.db
Note
By default Pylons configures your test suite so that the same code run by paster setup-app test.ini is run before your tests start. This can be confusing if you are creating tables and inserting data as mentioned in the previous section so you’ll want to comment that out and replace it with enough code to initialize your models.
Here’s a version of addressbook/tests/__init__.py that initializes your Pylons test suite for use with fixture. It creates and drops tables once per test run to reduce unnecessary overhead and exposes a global dbfixture object that other tests can import and use.
"""Pylons application test package
This package assumes the Pylons environment is already loaded, such as
when this script is imported from the `nosetests --with-pylons=test.ini`
command.
This module initializes the application via ``websetup`` (`paster
setup-app`) and provides the base testing objects.
"""
from unittest import TestCase
from paste.deploy import loadapp
from paste.script.appinstall import SetupCommand
from pylons import config, url
from routes.util import URLGenerator
from webtest import TestApp
# additional imports ...
from paste.deploy import appconfig
from addressbook.config.environment import load_environment
import pylons.test
# export dbfixture here for tests :
__all__ = ['environ', 'url', 'TestController', 'dbfixture']
# Invoke websetup with the current config file
##### comment this out so that initial data isn't loaded:
# SetupCommand('setup-app').run([config['__file__']])
##### but add this so that your models get configured:
appconf = appconfig('config:' + config['__file__'])
load_environment(appconf.global_conf, appconf.local_conf)
environ = {}
from addressbook import model
from addressbook.model import meta
from fixture import SQLAlchemyFixture
from fixture.style import NamedDataStyle
dbfixture = SQLAlchemyFixture(
env=model,
engine=meta.engine,
style=NamedDataStyle()
)
def setup():
meta.metadata.create_all(meta.engine)
def teardown():
meta.metadata.drop_all(meta.engine)
class TestController(TestCase):
def __init__(self, *args, **kwargs):
if pylons.test.pylonsapp:
wsgiapp = pylons.test.pylonsapp
else:
wsgiapp = loadapp('config:%s' % config['__file__'])
self.app = TestApp(wsgiapp)
url._push_object(URLGenerator(config['routes.map'], environ))
TestCase.__init__(self, *args, **kwargs)
def setUp(self):
# remove the session once per test so that
# objects do not leak from test to test
meta.Session.remove()
Note
Fixture deletes the rows it inserts. If your application inserts rows during a test then you will need to truncate the table or else go back to the strategy of creating / dropping tables per every test.
As illustrated by the test suite initialization code above, a common fixture can be used by all tests. It looks like:
dbfixture = SQLAlchemyFixture(
env=model,
engine=meta.engine,
style=NamedDataStyle()
)
See Using LoadableFixture for a detailed explanation of fixture objects.
Now let’s start working with the DataSet objects. Edit addressbook/tests/functional/test_book.py so that it looks like this:
from addressbook.model import meta, Person
from addressbook.datasets import PersonData, AddressData
from addressbook.tests import *
class TestBookController(TestController):
def setUp(self):
super(TestBookController, self).setUp()
self.data = dbfixture.data(PersonData) # AddressData loads implicitly
self.data.setup()
def tearDown(self):
self.data.teardown()
super(TestBookController, self).tearDown()
def test_index(self):
response = self.app.get(url(controller='book'))
print response
assert PersonData.joe_gibbs.name in response
assert PersonData.joe_gibbs.email in response
assert AddressData.joe_in_montego.address in response
assert AddressData.joe_in_ny.address in response
Then run the test, which should pass:
$ cd /path/to/addressbook
$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.702s
OK
Woo!
This code is asserting that the values from the DataSet classes have been rendered on the page, i.e. <h4>joe@joegibbs.com</h4>. There is more info on using response objects in the WebTest docs (however at the time of this writing Pylons is still using paste.fixture, an earlier form of WebTest).
You’ll notice there is a print statement showing the actual response. By default nose hides stdout for convenience so if you want to see the response just trigger a failure by adding raise AssertionError in the test.
$ nosetests
F
======================================================================
FAIL: test_index (addressbook.tests.functional.test_book.TestBookController)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/kumar/.../addressbook/tests/functional/test_book.py", line 16, in test_index
raise AssertionError
AssertionError:
-------------------- >> begin captured stdout << ---------------------
Response: 200
content-type: text/html; charset=utf-8
pragma: no-cache
cache-control: no-cache
<h2>
Address Book
</h2>
<h3>Joe Gibbs</h3>
<h4>joe@joegibbs.com</h4>
<h4>111 St. James St, Montego Bay, Jamaica</h4>
<h4>111 S. 2nd Ave, New York, NY</h4>
--------------------- >> end captured stdout << ----------------------
----------------------------------------------------------------------
Ran 1 test in 0.389s
FAILED (failures=1)
If you are using Session.mapper(TheClass, the_table) instead of just plain ol’ mapper(...) then you are introducing a potential problem in that your objects will save themselves to the wrong session. You’ll need to fix it by setting save_on_init=False like this:
meta.Session.mapper(Address, t_addresses, save_on_init=False)
meta.Session.mapper(Person, t_people, properties = {...}, save_on_init=False)
For convenience, this is the default behavior in Elixir. If working with Elixir Entities then construct your entities like this:
class Person(Entity):
name = Field(String(100))
email = Field(String(100))
has_many('addresses', of_kind='Address')
# :
using_mapper_options(save_on_init=False)
The side effect is that your app will always have to call person.save_or_update() whenever it wants to write data.
If you’ve seen an error during unload like:
UnloadError: InvalidRequestError: Instance 'Person@0x227d130' with key
(<class 'addressbook.model.Person'>, (1,), None) is already persisted with a different identity
(with <addressbook.model.Person object at 0x227d130> in
<PersonData at 0x2272450 with keys ['joe_gibbs']>)
then it probably means you have either called data.setup() twice without calling data.teardown() in between or else you somehow saved the same Person() object to two different sessions. If using an in-memory database be sure you have commented out the code that runs setup-app in tests/__init__.py (see above). You also might see this if you forget to set save_on_init=False to your mapped classes (also see above).
That’s it! Have fun.
This code is available from the fixture code repository in fixture/examples/pylons_example/addressbook.