I’ve been trying to find out why our Django unit tests were exhausting the memory on our development machine causing the OS to terminate our CI builds. Memory leaks are not normally associated with garbage collected languages such as Python so this was a curious problem. Our tests followed a fairly standard pattern:
from django.utils.unittest import TestCase from django.contrib.auth import User class ExampleTest(TestCase): def setUp(self): """ Set up any vars needed by the tests """ self.user = User.objects.get(pk=1) def test_firstname(self): """ Test the first name of our user """ self.assertEquals(self.user.first_name, "Chris") def test_active(self): """ Another test, this time checking is_active """ self.assertTrue(self.user.is_active)
By tracking the memory of tests like this it was obvious they would gobble up all available memory and once they did this the process would be terminated. Using guppy (and this excellent article on it’s usage) I discovered that after each test method, the memory was increasing (often by many megabytes per method). In our case the largest objects hanging around were Django Query objects. I realised that if we allocated data attributes in the setUp method and assigned them to self, they would never get garbage collected. A little more investigation revealed that every test method was getting a unique instance object (i.e. self is unique for each test). Trawling the documentation I found the offending line “Each instance of TestCase will run a single test method“. More problematic however is that after each test is run, a reference to the instance is retained (presumably for generating the results summary). Taken together this means that unless you explicitly delete data attributes in your tearDown() methods this data will hang around until the end of your tests (assuming you get there). In the above example you would end up with two User objects remaining in memory at the end of the test run, one per test method.
To solve the issue for our setup, I moved as much setUp() code into setUpClass() as possible and inherited all test classes from a base class which defined a tearDownClass method to remove any objects whose class name matches a given namespace.
from django.test import TestCase as _TestCase from django.contrib.auth import User class TestCase(_TestCase): @classmethod def tearDownClass(cls): # delete any class-level object instances in our namespace for name in dir(cls): try: myvalue = getattr(cls,name) t = str(type(myvalue)) if 'django.contrib.auth' in t: delattr(cls,name) except: pass class ExampleTest(TestCase): @classmethod def setUpClass(cls): """ Set up any vars needed by the tests, this time once per class rather then once per test method """ cls.user = User.objects.get(pk=1) def test_firstname(self): """ Test the first name of our user """ self.assertEquals(self.user.first_name, "Chris") def test_active(self): """ Another test, this time checking is_active """ self.assertTrue(self.user.is_active)
This is a simple solution which doesn’t stop leaks entirely (objects from classes outside our namespace or ones created in the setUp() method would still remain) but it does bring the memory management under control and allow all our tests to run.
UPDATE: It appears this bug has now been fixed in cPython