Modifying testutil.DBTest tearDown() to drop created tables

2 views
Skip to first unread message

Bill Woodward

unread,
Jun 16, 2006, 5:34:03 PM6/16/06
to turbo...@googlegroups.com
Howdy,

I've been running into some odd/unexpected behavior while trying to write and run some controller unit tests with TG 0.9.  I've been writing a testcase class that inherits from testutil.DBTest so I can set up some objects in the database and then use them within the controller test.  What I've found doing this is that I have to create a new set of objects, with different searchable values, for each test.  If I didn't do that, the old version of the object would get used, and values that I set in the object cretion would not be available.

This actually seems to be odd behavior to me.  I would actually expect values in the database to *not* persist between tests.  I've extracted some of the DBTest code into my own DBTest class and added code in the tearDown method to drop the tables that are used in the tests, so that the in-memory tables get recreated each time.

My test case class is:

--- begin IsolatedDBTestCase.py ---
import unittest
import inspect
import sqlobject
from sqlobject.inheritance import InheritableSQLObject
from turbogears import database, testutil

class IsolatedDBTestCase(unittest.TestCase):
    """
    The IsolatedDBTestCase class is a unit test testcase that creates
    the SQL tables in memory as needed, and drops them at the end of
    the test.  This isolates testcases that populate tables from each
    other and removes any order dependency.  The code for setUp comes
    directly from testutil.DBTest.  The tearDown code to drop the
    created tables is (obviously) a modified version of the setUp code,
    just altered to drop the tables after a rollback.
    """
    model = None

    def setUp(self):
        print "In setUp()"
        self.model = __import__("turbobudget", {}, {}, ["model"]).model

        for item in self.model.__dict__.values():
            if inspect.isclass(item) \
                and issubclass(item, sqlobject.SQLObject) \
                and item != sqlobject.SQLObject \
                and item != InheritableSQLObject:
                item.createTable(ifNotExists=True)

    def tearDown(self):
        database.rollback_all()  
        for item in self.model.__dict__.values():
            if inspect.isclass(item) \
                and issubclass(item, sqlobject.SQLObject) \
                and item != sqlobject.SQLObject \
                and item != InheritableSQLObject:
                item.dropTable()
--- end IsolatedDBTestCase.py ---

I ripped out some of the code that makes DBTest more generic, since I refactored this out for my own testing, but the important work is in the tearDown() method, which essentially just drops any of the tables that were created in the setUp() method.

My controller testcases now look approximately like:

--- begin ---
from turbobudget.controllers import Root
from turbobudget.model import Budget, Envelope, User, Expense
import cherrypy
from turbogears.database import PackageHub
from turbogears import testutil
from IsolatedDBTestCase import IsolatedDBTestCase

cherrypy.root = Root()

class TestEnvelopeMethods(IsolatedDBTestCase):
    def test_getEnvelope(self):
        "Set up an envelope and retrieve it"
       
        # Arrange
        #Budget._connection.debug = True
        e1 = Envelope(name = "E1", description = "E1",
                      budget = None, value = 500)
        id = e1.id
       
        # Act
        result = testutil.call(cherrypy.root.getEnvelope,
                               name = "E1")
       
        # Assert
        assert result['name'] == "E1"
        assert result['value'] == 500

    def test_getEnvelopeWithValue(self):
        "Set up an envelope with a value and retrieve it"
       
        # Arrange
        #Budget._connection.debug = True
        e1 = Envelope(name = "E1", description = "E1",
                      budget = None, value = 1000)
        id = e1.id
       
        # Act
        result = testutil.call(cherrypy.root.getEnvelope,
                               name = "E1")
       
        # Assert
        assert result['name'] == "E1"
        assert result['value'] == 1000, "Got %s" % result['value']
       
    def test_tallyEnvelopeWithOneExpense(self):
        "Set up an envelope with one expense and tally it"
       
        # Arrange
        #Budget._connection.debug = True
        e1 = Envelope(name = "E1", description = "E1",
                      budget = None, value = 750)
        ex1 = Expense(description = "Expense 1", category = "Test",
                      amount = 8.00, envelope = e1)
       
        # Act
        result = testutil.call(cherrypy.root.getEnvelope,
                               name = "E1")
       
        # Assert
        assert result['name'] == "E1"
        assert result['value'] == 750.0 - 8.0, "Got %s" % result['value']
       
    def test_tallyEnvelopeWithExpenses(self):
        "Set up an envelope with expenses and tally it"
       
        # Arrange
        #Budget._connection.debug = True
        e1 = Envelope(name = "E1", description = "E1",
                      budget = None, value = 825)
        ex1 = Expense(description = "Expense 1", category = "Test",
                      amount = 8.00, envelope = e1)
        ex2 = Expense(description = "Expense 2", category = "Test",
                      amount = 14.00, envelope = e1)
        ex3 = Expense(description = "Expense 3", category = "Test",
                      amount = 21.00, envelope = e1)
       
        # Act
        result = testutil.call(cherrypy.root.getEnvelope,
                               name = "E1")
       
        # Assert
        assert result['name'] == "E1"
        assert result['value'] == 825.0 - 43.0, "Got %s" % result['value']
--- end ---

Before I made the tearDown() change, I had to use a new name for each envelope to keep the in-controller lookup ( Envelope.select(...)) from always getting the first one that was created. 

I hope someone finds this useful.  To me, this seems like a more unit-test friendly behavior for tearDown(). 

- Bill

--
Bill Woodward     wpw...@gmail.com       http://www.saifa.net

"I have more trouble with D. L. Moody than with any other man I ever met." -- D. L. Moody
        s/D. L. Moody/Bill Woodward/g

Jason Chu

unread,
Jun 16, 2006, 7:15:35 PM6/16/06
to turbo...@googlegroups.com
On Fri, 16 Jun 2006 16:34:03 -0500
"Bill Woodward" <wpw...@gmail.com> wrote:

> Howdy,
>
> I've been running into some odd/unexpected behavior while trying to
> write and run some controller unit tests with TG 0.9. I've been
> writing a testcase class that inherits from testutil.DBTest so I can
> set up some objects in the database and then use them within the
> controller test. What I've found doing this is that I have to create
> a new set of objects, with different searchable values, for each
> test. If I didn't do that, the old version of the object would get
> used, and values that I set in the object cretion would not be
> available.
>
> This actually seems to be odd behavior to me. I would actually expect
> values in the database to *not* persist between tests. I've
> extracted some of the DBTest code into my own DBTest class and added
> code in the tearDown method to drop the tables that are used in the
> tests, so that the in-memory tables get recreated each time.

It is odd behavior. When I first wrote the DBTest class, I meant for
it to do that. It looks like, in more cases than not, sqlite flubs
transactions.

I believe you're seeing this problem because the transaction is
commited by the auto-commit after a request is done.

I agree that dropping the tables is a really clean way to do it.

Because this is the way that DBTest is really supposed to work and I
don't know if anyone actually depends on the old, broken method, I vote
this gets added before 1.0. Strictly speaking, I don't know if it's
considered an API change.

Jason

signature.asc

Kevin Dangoor

unread,
Jun 16, 2006, 10:24:03 PM6/16/06
to turbo...@googlegroups.com
On Jun 16, 2006, at 7:15 PM, Jason Chu wrote:

> Because this is the way that DBTest is really supposed to work and I
> don't know if anyone actually depends on the old, broken method, I
> vote
> this gets added before 1.0. Strictly speaking, I don't know if it's
> considered an API change.

Generally speaking, you're correct that unit tests should clean up
after themselves. I'm a little more lenient with changes to the
testing code, because if someone's using tests they'll immediately
see if they have anything they need to patch up.

Kevin

Reply all
Reply to author
Forward
0 new messages