I have taken a quick look at this code.  But be warned I have not
actually used it yet so I have limited "real" experience with it...

Overall it looks very interesting.  I was expecting something much
more along the vein of DbUnit where you do something like this:

setUp():
   dataset = XmlDataset("dataset.xml")
   db.refresh(dataset)

tearDown():
   db.tearDown()

testMethod():
   # Do normal db queries here relying upon the data to be in there
   # some custom comparisons against loaded datasets are supported

You can get the idea of DbUnit by look at it's howto page:
http://dbunit.sourceforge.net/howto.html

So far the fixture code looks a little magic to me but that may be
because I do not use SessionContext or assign_mapper in my code.  I
think it would be helpful to see a full example of a simple test case
class (using unittest) that only uses the standard SA features found
in the SA tutorial.  This may help separate the SA plugin magic from
the fixture magic.

Anyway, see below for more detailed comments....

On 2/6/07, Kumar McMillan <[EMAIL PROTECTED]> wrote:
>
> On 2/3/07, Allen Bierbaum <[EMAIL PROTECTED]> wrote:
> >
> > This works for creating the table structure, and that is important,
> > but for me I think the actual data in the database is just was
> > important if not more so. What are people using to populate the
> > database with test data to use in the tests?
>
> if you feel adventurous, I've been working to implement SQLAlchemy in
> the fixture module :
>
> http://code.google.com/p/fixture/
>
> It's not too difficult to run insert statements at the top of your
> tests, or inject an SQL file.  But when you want to make assertions
> using that data things get tricky, which is why I created the fixture
> module.  To give you a very rough example of how this works (since the
> docs are very lacking at the moment), you have 1) DataSet objects to
> define data and 2) a Fixture object that knows how to load that data.
>
> In fact, special for you--!--and because I've been meaning to do so, I
> just checked in this example, to illustrate the point (if formatting
> goes funny, it is here:
> http://fixture.googlecode.com/svn/trunk/fixture/examples/db/sqlalchemy_examples.py
> ).  Before I get to it, I want to quickly address the point you made
> about having a gigabyte database but only needing a sample of it for
> testing.  I actually find the simplest way to work with fixtures is to
> use existing data.  So the module also includes a command line that
> can generate fixture code from a data source, given an object that
> knows how to select.  I.E.
>
> $ fixture path.to.a.table.widgets --dsn=postgres://[EMAIL PROTECTED]/db
> --query="widget_id=2"
>
> will build fixture code (including all foreign key refs) by creating a
> select statement using the widgets table.  This command isn't
> implemented fully and may be rough around the edges but it works for
> something like above.

This command looks interesting and I need to try it on some of my
"real" data.  One question though, it looks like this generates python
code correct?  Have you thought about supporting the option of
generating some metadata xml file that can be loaded directly.  This
may make maintenance a little easier and the datasets less verbose.
It could also allow for hand editing of the xml datasources when that
makes sense.

> Now ... that example:
>
>
> SequencedSet Fixtures with SQLAlchemy and nose
> ----------------------------------------------
>
> Create your tables::
>
>     >>> from sqlalchemy import *
>     >>> from sqlalchemy.ext.sessioncontext import SessionContext
>     >>> from sqlalchemy.ext.assignmapper import assign_mapper
>
>     >>> meta = BoundMetaData("sqlite:///:memory:")
>     >>> session_context = SessionContext(
>     ...     lambda: create_session(bind_to=meta.engine))
>     ...
>     >>> affiliates = Table('affiliates', meta,
>     ...     Column('id', INT, primary_key=True),
>     ...     Column('name', String),)
>     ...
>     >>> class Affiliate(object): pass
>     >>> m = assign_mapper(session_context, Affiliate, affiliates)
>     >>> events = Table('events', meta,
>     ...     Column('id', INT, primary_key=True),
>     ...     Column('type', String),
>     ...     Column('affiliate_id', INT,
>     ...         ForeignKey('affiliates.id')),)
>     ...
>     >>> class Event(object): pass
>     >>> m = assign_mapper(session_context, Event, events, properties = {
>     ...                                 'affiliate': relation(Affiliate), })
>
> Note that using mappers above is not necessary.  The fixture module also
> supports interacting with mapper classes, however.
>
> Next you build the DataSet objects that you want to load, in this case they
> inherit from SequencedSet, an optional DataSet enhancement that simulates
> auto-incrementing IDs.  The IDs values can be overridden for any row and the
> column name is configurable, but defaults to 'id'::

Why is this needed?  If the id is already a sequence shouldn't the
standard inserts into the database handle this automatically?  How can
it be disabled?

>
>     >>> from fixture import SequencedSet
>     >>> class affiliates_data(SequencedSet):
>     ...     class joe:
>     ...         name="Joe, The Affiliate"
>     ...
>     >>> class events_data(SequencedSet):
>     ...     class joes_click:
>     ...         affiliate_id = affiliates_data.joe.ref('id')
>     ...         type="click"
>     ...     class joes_submit(joes_click):
>     ...         type="submit"
>     ...     class joes_activation(joes_click):
>     ...         type="activation"
>     ...
>
> Note how joes_submit inherits from joes_click.  Inheritance is a natural way 
> to
> share common column values (in this case, all events belong to the same
> affiliate).

This structure was very interesting and IMHO innovative.  Basically it
represents the source "data" as a set of first class python objects
that can be referenced later.  I can see how this could be very
helpful for small test cases.  I am a little less sure of how useful
this would be for a larger set of data.  For example if I need to test
on a complex dataset where I have 20-30 tables and 10-20 rows in each
table then things get a little more tricky.  But maybe I should be
focusing on simplifying my data in this case. :)

> Next you need a module-level Fixture instance that knows how to load the above
> DataSet object(s).  We are also going to tell it to derive SQLAlchemy table
> names by looking in the global scope and chopping off "_data" from the DataSet
> class name (there are other ways to do this more or less explicitly).
>
> We are going to pass it a session_context to create connections with, but 
> again
> there are alternatives to this::
>
>     >>> from fixture.style import TrimmedNameStyle
>     >>> from fixture import SQLAlchemyFixture
>     >>> db = SQLAlchemyFixture( env=globals(), 
> session_context=session_context,
>     ...                         style=TrimmedNameStyle(suffix="_data"))
>     ...

Using the name "db" here confused me at first since I normal name
engines something with "db" in the name.  But I think I see the point
where this is creating a fixture and letting the fixture know about
the datasets it can load.  What I don't understand though is if it is
using the TrimmedNameStyle to pick up the data sets from above, then
why do they have to be passed in explicitly to the db.with_data
decorator below?

> Now we are ready to write a test that uses the fixtures.  The
> following is just one way you could write a test function runnable by
> nose_ ::
>
>     >>> def setup_data():
>     ...     meta.create_all()
>     ...
>     >>> def teardown_data():
>     ...     meta.drop_all()
>     ...     clear_mappers()
>     ...     # and do whatever else ...

Is it possible to capture all of the meta create/drop and data loading
behind the scenes with some standard calls.  For example something
like:

def setUp():
   db.setupDatabase(method=CLEAN_ALL)
def tearDown():
   db.teardownDatabase(method=DELETE_ALL)

CLEAN_ALL and DELETE_ALL are ideas from DbUnit.  They let the fixture
infrastructure know that it should clear everything and load the
everything it knows about the database from scratch (CLEAN_ALL) and
delete everything from the database that the fixture knows about
(DELETE_ALL).

>     >>> @db.with_data(events_data, setup=setup_data, teardown=teardown_data)
>     ... def test_event_something(data):
>     ...     joe = Affiliate.get(data.affiliates_data.joe.id)
>     ...     click = Event.get(data.events_data.joes_click.id)
>     ...     assert click.affiliate is joe
>     ...     assert click.type == data.events_data.joes_click.type
>     ...
>
> The rest will be done for you automatically by nose_::
>
>     >>> import nose, unittest
>     >>> result = unittest.TestResult()
>     >>> case = nose.case.FunctionTestCase(test_event_something)
>     >>> case(result)
>     >>> result.testsRun
>     1
>     >>> result.errors
>     []
>     >>> result.failures
>     []
>
> Here are some things to note.  @db.with_data() takes an arbitrary number of
> DataSet classes as an argument and passes an instance of Fixture.Data to the
> test function.  Because of the reference to affiliates_data, you don't have to
> specify that set since it will be discovered by reference.  Also note
> that there
> are other ways to load data.  Say, if you want to work with unittest.TestCase
> classes you could create the data instance you see above manually in a setUp()
> def, like data = db.data(events_data)

This makes sense to me now.  The one point of confusion I have is how
a session would fit into all this.  In your case I think it is being
hidden by the plugins you are using but since I don't have those I
think I may have to handle something more explicitly.  (or maybe I
should be using the plugins. :)

Thanks for the great code.  It looks very helpful and I can't wait to
use it.  Please understand that my comments are not meant to be harsh
or critical, just some feedback from someone looking at the code for a
first time.

Thanks,
Allen

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To post to this group, send email to sqlalchemy@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to