|Ned Batchelder : Blog | Code | Text | Site|
» Home : Blog : August 2011
Running a Django test suite today, we had a frustrating problem: database objects were leaking from one test to the next. To track down how this was happening, I used some aggressive debugging tricks.
First, a little background: Django provides two test case classes for creating tests: TestCase, which should usually be used, and TransactionTestCase, which should be used for testing code that does explicit transaction management.
TestCase uses transactions to restore the state of the database at the end of each test. This helps guarantee test isolation, an important property of tests that ensures that each tests runs without being tainted by the side effects of previous tests.
We didn't have any TransactionTestCase classes in our code, but we were definitely seeing bleed-through from one test to the next. One particular test failed unless it deleted all User objects at the very beginning. But there shouldn't have been any Users in the database at the start of the test in the first place. Clearly, some database state wasn't being restored after some test: there was one User object left in the database when it should have been empty.
I knew what test was tripped up by the User object, but which test left it behind? There are hundreds of tests, and many of them create User objects. Reading the code wasn't revealing anything. A more powerful technique would be needed.
It's easy in the failing test to examine the errant User object. What if the User object could itself point to where it had been created? If the creator of User objects annotated the object with a stack trace, then we could examine that stack in the failing test, and we'd identify the culprit.
Keep in mind here: my plan wasn't to add features to Django, or to our product code, or even our tests. I was going to hack whatever was needed to find the test that polluted the database, and then I was going to get rid of it all. So all sorts of dirty tricks were allowable.
Getting a useful stack trace at a point in the code isn't difficult: inspect.stack gives us the stack itself, and from there we can pull useful information to put into a string:
Calling this function returns a string that looks like this:
Every frame in the stack is a single line, with the file name, line number, function name, and if present, the value of self. Getting the stack string is simple enough: User objects are all created with a helper function called create_user in django/contrib/auth/models.py. Adding a call to get_stack there will get us the stack trace we want.
But where to store it in User? Ideally there'd be a description column or something that we could stuff this into, but there isn't. I tried storing it as the password, but that failed miserably, because the password is hashed before storing, and then dozens of tests failed when the passwords didn't match. If these had been ordinary Python objects, I could have just added new attributes to store the stack trace, but these were written to the database, then later read back out, so only information that got stored in the database was useful to me.
So I was a bit stuck: User had no usable text column in which to store the stack trace, so I couldn't annotate the User object itself. Instead, maybe I could store the stack trace somewhere else, and associate it with the User object. My first thought was to use the primary key of the User as an identifier, but of course, as the database is rolled back and Users created, they re-use the same keys, so that was no good.
Then I noticed that Users are created with the current time as their last_login value. Those datetimes have enough resolution that each User gets a unique value, and of course, time progresses independently of database rollbacks, so there's no danger of values getting reused.
Now I had a plan: modify django/contrib/auth/models.py to keep a global dictionary mapping User creation times to the stack trace that created them. Then, in my failing test, I could examine the User object, and use its last_login time to look up the stack trace.
I modified the creation code like this (simplified):
Then, in the failing test, I added a check at the very beginning, where we expected to have no Users:
With these modifications in place, running the tests gave me just what I wanted: a stack trace that pinpointed where the User was created. The actual trace is the one I've included at the top of this post. The most useful part of the stack was this line:
which shows that the User was created in the setUp method of the test_create_user test. Looking at that test showed what the problem was:
As I mentioned at the very beginning, Django provides two test case classes to derive from. But here's a test that isn't part of a class at all! Nose allows the use of test functions in addition to the classic unittest-style test classes. Many people like the cleaner Pythonic feel of functions. But Django relies on the setup and teardown that the test case classes provide. Using pure test functions in a Django project is a good way to miss out on the machinery that cleans the databases between tests, which is precisely what we'd been experiencing.
Removing this test (it also happened to be redundant!) solved the problem.