Wednesday, August 25, 2010

How Python's unittest library figures out what functions to run..

Django's django.test.TestCase ultimately inherits from the Python unittest library, so I was curious to figure out how Python actually discerns what test functions to run. For instance, how does unittest.TestCase() ultimately know that there are two test methods (test_details(), and test_index()) to invoke?
from django.test import TestCase

class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get('/customer/details/')
        self.failUnlessEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get('/customer/index/')
        self.failUnlessEqual(response.status_code, 200)
Well, we can start by tracing into the django.test.TestCase code:

django.test.testcases.py:

class TestCase(TransactionTestCase):

...inherits:

class TransactionTestCase(unittest.TestCase):

..
unittest.py (Python library):

class TestCase:
    """A class whose instances are single test cases.

    By default, the test code itself should be placed in a method named
    'runTest'.

When you invoke python manage.py test, Django calls django.test.simple.DjangoTestSuiteRunner, which inherits from unittest.TextTestRunner. Inside django.test.simple.py, there is this following line:

suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_module))

The actual instantiation is declared inside unittest.py:

defaultTestLoader = TestLoader()

The loadTestsfromModule() first get invokes, which in turn invokes getTestCaseNames():

unit.test.py:
def loadTestsFromTestCase(self, testCaseClass):
        """Return a suite of all tests cases contained in testCaseClass"""
        if issubclass(testCaseClass, TestSuite):
            raise TypeError("Test cases should not be derived from TestSuite. Maybe you meant to derive from TestCase?")
        testCaseNames = self.getTestCaseNames(testCaseClass)
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        return self.suiteClass(map(testCaseClass, testCaseNames))

The method getTestCaseNames() that searches for all methods that begin with "test_" and are callable:
def getTestCaseNames(self, testCaseClass):
        """Return a sorted sequence of method names found within testCaseClass
        """
        def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=self.testMethodPrefix):
            return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))
        testFnNames = filter(isTestMethod, dir(testCaseClass))
        for baseclass in testCaseClass.__bases__:
            for testFnName in self.getTestCaseNames(baseclass):
                if testFnName not in testFnNames:  # handle overridden methods
                    testFnNames.append(testFnName)
        if self.sortTestMethodsUsing:
            testFnNames.sort(self.sortTestMethodsUsing)
        return testFnNames
So if you invoked the method directly through the Python interpreter, you can see how this function works:

>>> import unittest
>>> b = unittest.TestLoader()
>>> b.getTestCaseNames(SimpleTest)
['test_details', 'test_index']

No comments:

Post a Comment