Commits

Anonymous committed f3b3b5b Merge

Merged upstream

Comments (0)

Files changed (6)

     * a doctest loader (looks for doctests in all text files in the project)
     * use a regex for matching files in test discovery instead of a glob
     * growl notifications on test run start and stop
-    * filter individual *test methods* using a regex
+    * filter individual test methods using a regex
     * load test functions from modules as well as TestCases
+    * test generators
     * integration with the coverage module for coverage reporting
     * display the time of individual tests in verbose reports
     * display a progress indicator as tests are run ([39/430] format) in verbose reports
 * ``testMethodPrefix`` - set to None, modify this attribute to *change* the prefix being used for this class
 * ``extraNames`` - a list of extra names to use for this test case as well as the default ones
 * ``excludedNames`` - a list of names to exclude from loading from this class
+* ``isTestMethod`` - the default filter for telling if a name is a valid test method name
 
 This event can be handled. If it is handled it should return a list of strings. Note that if this event returns an empty list (or None which will be replaced with an empty list then ``loadTestsFromTestCase`` will still check to see if the TestCase has a ``runTest`` method.
 
 Even if the event is handled ``extraNames`` will still be added to the list, however *excludedNames`` won't be removed as they are filtered out by the default implementation which looks for all attributes that are methods (or callable) whose name begins with ``loader.testMethodPrefix`` (or ``event.testMethodPrefix`` if that is set) and aren't in the list of excluded names (converted to a set first for efficient lookup).
 
+Note that modifying ``isTestMethod`` has no effect. It is there as a convenience for plugins wanting to be able to use the default check.
+
 The list of names will also be sorted using ``loader.sortTestMethodsUsing``.
 
 
 
 Add an epilogue to the optparse help messages.
 
-How do we provide paramterized / generative tests?
+In the test generator plugin exceptions raised whilst loading the tests should
+become a failed test rather than bombing out of test loading. We could still
+add paramaterized tests as well as generated tests.
 
 If the merge to unittest in Python 3.2 happens we need to decide which of the
 example plugins will be included in unittest. (They will probably all remain
 
 [coverage]
 always-on = False
+# location of coverage config file
 config =
 report-html = False
 # only used if report-html is false
 [doctest]
 always-on = False
 
-[module-loading]
+[functions]
+always-on = False
+
+[generators]
 always-on = False
 
 [checker]

unittest2/case.py

         """
         self._testMethodName = methodName
         self._resultForDoCleanups = None
+
         try:
             testMethod = getattr(self, methodName)
         except AttributeError:
             raise ValueError("no such test method in %s: %s" % \
                   (self.__class__, methodName))
+    
         self._testMethodDoc = testMethod.__doc__
+            
         self._cleanups = []
 
         # Map types to custom assertEqual functions that will compare

unittest2/events.py

         self.extraTests = []
 
 class GetTestCaseNamesEvent(_Event):
-    def __init__(self, loader, testCase):
+    def __init__(self, loader, testCase, isTestMethod):
         _Event.__init__(self)
         self.loader = loader
         self.testCase = testCase
         self.testMethodPrefix = None
         self.extraNames = []
         self.excludedNames = []
+        self.isTestMethod = isTestMethod
 
 class RunnerCreatedEvent(_Event):
     def __init__(self, runner):

unittest2/loader.py

     def getTestCaseNames(self, testCaseClass):
         """Return a sorted sequence of method names found within testCaseClass
         """
-        event = GetTestCaseNamesEvent(self, testCaseClass)
+        excluded = set()
+        def isTestMethod(attrname, testCaseClass=testCaseClass,
+                         excluded=excluded):
+            prefix = event.testMethodPrefix or self.testMethodPrefix
+            return (
+                attrname.startswith(prefix) and 
+                hasattr(getattr(testCaseClass, attrname), '__call__') and
+                attrname not in excluded
+            )
+        event = GetTestCaseNamesEvent(self, testCaseClass, isTestMethod)
+
         result = hooks.getTestCaseNames(event)
         if event.handled:
             testFnNames = result or []
         else:
-            prefix = event.testMethodPrefix or self.testMethodPrefix 
-            excluded = set(event.excludedNames)
-            def isTestMethod(attrname, testCaseClass=testCaseClass,
-                                 prefix=prefix, excluded=excluded):
-                return (
-                    attrname.startswith(prefix) and 
-                    hasattr(getattr(testCaseClass, attrname), '__call__') and
-                    attrname not in excluded
-                )
+            excluded = excluded.update(event.excludedNames)
             testFnNames = filter(isTestMethod, dir(testCaseClass))
         if event.extraNames:
             testFnNames.extend(event.extraNames)

unittest2/plugins/moduleloading.py

-from unittest2 import Plugin, FunctionTestCase
+from unittest2 import Plugin, FunctionTestCase, TestCase
 
 import types
 
-help_text = 'Load test functions from test modules'
-class TestLoading(Plugin):
-    
-    configSection = 'module-loading'
-    commandLineSwitch = (None, 'test-functions', help_text)
-
-
-    def loadTestsFromModule(self, event):
-        loader = event.loader
-        module = event.module
-        
-        def is_test(obj):
-            return obj.__name__.startswith(loader.testMethodPrefix)
-        
-        tests = []
-        for name in dir(module):
-            obj = getattr(module, name)
-            if isinstance(obj, types.FunctionType) and is_test(obj):
-                args = {}
-                setUp = getattr(obj, 'setUp', None)
-                tearDown = getattr(obj, 'tearDown', None)
-                if setUp is not None:
-                    args['setUp'] = setUp
-                if tearDown is not None:
-                    args['tearDown'] = tearDown
-                case = FunctionTestCase(obj, **args)
-                tests.append(case)
-                
-        event.extraTests.extend(tests)
 
 def setUp(setupFunction):
     def decorator(func):
 
 def testGenerator(func):
     func.testGenerator = True
-    return func
+    return func
+
+class Functions(Plugin):
+    
+    generatorsEnabled = False
+    configSection = 'functions'
+    commandLineSwitch = (None, 'functions', 'Load tests from functions')
+
+    def loadTestsFromModule(self, event):
+        loader = event.loader
+        module = event.module
+        
+        def is_test(obj):
+            if obj is testGenerator:
+                return False
+            return obj.__name__.startswith(loader.testMethodPrefix)
+        
+        tests = []
+        for name in dir(module):
+            obj = getattr(module, name)
+            if isinstance(obj, types.FunctionType) and is_test(obj):
+                args = {}
+                setUp = getattr(obj, 'setUp', None)
+                tearDown = getattr(obj, 'tearDown', None)
+                if setUp is not None:
+                    args['setUp'] = setUp
+                if tearDown is not None:
+                    args['tearDown'] = tearDown
+                
+                if (not self.generatorsEnabled or 
+                    getattr(obj, 'testGenerator', None) is None):
+                    case = FunctionTestCase(obj, **args)
+                    tests.append(case)
+                else:
+                    extras = list(obj())
+                    name = '%s.%s' % (obj.__module__, obj.__name__)
+                    def createTest(name):
+                        return GeneratorFunctionCase(name, **args)
+                    tests.extend(testsFromGenerator(name, extras, createTest))
+                
+        event.extraTests.extend(tests)
+
+
+class GeneratorFunctionCase(FunctionTestCase):
+
+    def __init__(self, name, **args):
+        self._name = name
+        FunctionTestCase.__init__(self, None, **args)
+
+    _testFunc = property(lambda self: getattr(self, self._name),
+                         lambda self, func: None)
+
+    def __repr__(self):
+        return self._name
+
+    id = __str__ = __repr__
+
+
+class Generators(Plugin):
+
+    configSection = 'generators'
+    commandLineSwitch = (None, 'generators', 'Load tests from generators')
+
+    def pluginsLoaded(self, event):
+        Functions.generatorsEnabled = True
+        
+    def loadTestsFromTestCase(self, event):
+        testCaseClass = event.testCase
+        for name in dir(testCaseClass):
+            method = getattr(testCaseClass, name)
+            if getattr(method, 'testGenerator', None) is not None:
+                instance = testCaseClass(name)
+                tests = list(method(instance))
+                event.extraTests.extend(
+                    testsFromGenerator(name, tests, testCaseClass)
+                )
+
+    def getTestCaseNames(self, event):
+        names = filter(event.isTestMethod, dir(event.testCase))
+        klass = event.testCase
+        for name in names:
+            method = getattr(klass, name)
+            if getattr(method, 'testGenerator', None) is not None:
+                event.excludedNames.append(name)
+
+def testsFromGenerator(name, tests, testCaseClass):
+    for index, (func, args) in enumerate(tests):
+        summary = ', '.join(repr(arg) for arg in args)
+
+        method_name = '%s_%s\n%s' % (name, index + 1, summary[:79])
+        setattr(testCaseClass, method_name, None)
+        instance = testCaseClass(method_name)
+        delattr(testCaseClass, method_name)
+        def method(func=func, args=args):
+            return func(*args)
+        setattr(instance, method_name, method)
+        yield instance