Commits

Antti Kaihola  committed 4610062

Added support for isolating methods in classes and instances with DingusTestCase

  • Participants
  • Parent commits 28db589

Comments (0)

Files changed (3)

 
 
 import sys
+from copy import copy
 from functools import wraps
+from inspect import isclass
+from types import MethodType
 
-def DingusTestCase(object_under_test, exclude=None):
+
+def clone_class(cls, exclude=None):
+    exclude = [] if exclude is None else exclude
+    clone_attrs = {}
+    for key, value in cls.__dict__.iteritems():
+        if key == '__dict__':
+            continue
+        if key in exclude:
+            clone_attrs[key] = value
+        else:
+            clone_attrs[key] = Dingus()
+            if key == '__init__':
+                clone_attrs[key].return_value = None
+    return type(cls.__name__, cls.__bases__, clone_attrs)
+
+
+def clone_instance(obj, exclude=None):
+    exclude = [] if exclude is None else exclude
+    clone = copy(obj)
+    for key in dir(obj):
+        if key in ('__class__', '__dict__', '__weakref__'):
+            continue
+        if key in exclude:
+            setattr(clone, key, getattr(obj, key))
+        else:
+            setattr(clone, key, Dingus())
+            #if key == '__init__':
+            #    clone_attrs[key].return_value = None
+    return clone
+
+
+def clone_object(obj, exclude=None):
+    if isclass(obj):
+        return clone_class(obj, exclude=exclude)
+    return clone_instance(obj, exclude=exclude)
+
+
+def DingusTestCase(item_under_test, exclude=None):
+    if isinstance(item_under_test, MethodType):
+        object_under_test = item_under_test.im_self or item_under_test.im_class
+        method_under_test = item_under_test
+    else:
+        object_under_test = item_under_test
+        method_under_test = None
+
     exclude = [] if exclude is None else exclude
 
     def get_names_under_test():
             if value is object_under_test or name in exclude:
                 yield name
 
+    def get_attribs_under_test():
+        if method_under_test:
+            for name in dir(object_under_test):
+                value = getattr(object_under_test, name)
+                is_method_under_test = (
+                    callable(value)
+                    and value.im_func == method_under_test.im_func)
+                if is_method_under_test or '.%s' % name in exclude:
+                    yield name
+
     class TestCase(object):
         def setup(self):
             module_name = object_under_test.__module__
 
             dunders = set(k for k in module_keys
                            if k.startswith('__') and k.endswith('__'))
-            replaced_keys = (module_keys - dunders - set(names_under_test))
+            replaced_keys = (module_keys - dunders - set(exclude))
+            if method_under_test:
+                cloned_object = clone_object(object_under_test,
+                                             exclude=attribs_under_test)
             for key in replaced_keys:
-                module.__dict__[key] = Dingus()
+                old_value = old_module_dict[key]
+                if method_under_test and old_value == object_under_test:
+                    module.__dict__[key] = cloned_object
+                elif old_value is not object_under_test:
+                    module.__dict__[key] = Dingus()
             module.__dict__['__dingused_dict__'] = old_module_dict
 
         def _dingus_restore_module(self, module):
             module.__dict__.update(old_module_dict)
 
     names_under_test = list(get_names_under_test())
-    TestCase.__name__ = '%s_DingusTestCase' % '_'.join(names_under_test)
+    attribs_under_test = list(get_attribs_under_test())
+    TestCase.__name__ = '%s_DingusTestCase' % '_'.join(
+        names_under_test + ['ATTRIB%s' % n for n in attribs_under_test])
     return TestCase
 
 

File tests/test_case_fixture.py

 atomic_value = 'foo'
 
+
+class CloneableClass(object):
+    class_attribute = 'class attribute'
+
+    def __init__(self):
+        self.instance_attribute = 'instance attribure'
+        return None
+
+    def instance_method(self):
+        return 'instance method'
+
+    @classmethod
+    def class_method(cls):
+        return 'class method'
+
+    @staticmethod
+    def static_method(cls):
+        return 'static method'
+
+
+cloneable_instance = CloneableClass()
+
+
 class ClassUnderTest:
-    pass
+    def method_under_test(self):
+        return 'return value of undingused method under test'
+    def collaborator_method(self):
+        return 'undingused return value of collaborator method'
+
+
+instance_under_test = ClassUnderTest()
+
+
 class Collaborator:
     pass
-

File tests/test_dingus_test_case.py

+from nose.tools import eq_
 from tests import test_case_fixture as module
-from tests.test_case_fixture import ClassUnderTest, Collaborator
+from tests.test_case_fixture import (
+    ClassUnderTest, Collaborator, instance_under_test)
 
 from dingus import DingusTestCase, Dingus
 
         self.test_case_instance.teardown()
 
 
+class WhenMethodIsExcludedFromTest:
+    def setup(self):
+        class TestCase(DingusTestCase(module.ClassUnderTest.method_under_test,
+                                      exclude=['.collaborator_method'])):
+            pass
+        self.test_case_instance = TestCase()
+        self.test_case_instance.setup()
+
+    def should_not_replace_it_with_dingus(self):
+        assert (module.ClassUnderTest.collaborator_method.im_func
+                is ClassUnderTest.collaborator_method.im_func)
+
+    def teardown(self):
+        self.test_case_instance.teardown()
+
+
 class WhenCallingSetupFunction:
     def setup(self):
         class TestCase(DingusTestCase(module.ClassUnderTest)):
     def should_leave_globals_as_they_were_before_dingusing(self):
         assert module.__dict__ == self.original_module_dict
 
+
+class WhenCallingSetupFunctionAndTestingMethod:
+    def setup(self):
+        class TestCase(DingusTestCase(module.ClassUnderTest.method_under_test)):
+            pass
+        self.test_case_instance = TestCase()
+        self.test_case_instance.setup()
+
+    def teardown(self):
+        self.test_case_instance.teardown()
+
+    def should_not_replace_module_dunder_attributes(self):
+        assert isinstance(module.__name__, str)
+        assert isinstance(module.__file__, str)
+
+    def should_replace_module_non_dunder_attributes(self):
+        assert isinstance(module.atomic_value, Dingus)
+
+    def should_replace_collaborating_classes(self):
+        assert isinstance(module.Collaborator, Dingus)
+
+    def should_replace_class_under_test_with_clone(self):
+        assert module.ClassUnderTest is not ClassUnderTest
+
+    def should_leave_method_under_test_intact(self):
+        assert (module.ClassUnderTest.method_under_test.im_func
+                == ClassUnderTest.method_under_test.im_func)
+        assert (module.ClassUnderTest().method_under_test()
+                == 'return value of undingused method under test')
+
+    def should_replace_other_methods(self):
+        assert isinstance(module.ClassUnderTest.collaborator_method, Dingus)
+
+
+class WhenCallingTeardownFunctionAndTestingMethod:
+    def setup(self):
+        self.original_module_dict = module.__dict__.copy()
+        class TestCase(DingusTestCase(module.ClassUnderTest.method_under_test)):
+            pass
+        test_case_object = TestCase()
+        test_case_object.setup()
+        test_case_object.teardown()
+
+    def should_restore_module_attributes(self):
+        assert module.atomic_value is 'foo'
+
+    def should_leave_globals_as_they_were_before_dingusing(self):
+        assert module.__dict__ == self.original_module_dict
+
+
+class WhenCallingSetupFunctionAndTestingMethodOfInstance:
+    def setup(self):
+        class TestCase(
+            DingusTestCase(
+                module.instance_under_test.method_under_test)):
+            pass
+        self.test_case_instance = TestCase()
+        self.test_case_instance.setup()
+
+    def teardown(self):
+        self.test_case_instance.teardown()
+
+    def should_not_replace_module_dunder_attributes(self):
+        assert isinstance(module.__name__, str)
+        assert isinstance(module.__file__, str)
+
+    def should_replace_module_non_dunder_attributes(self):
+        assert isinstance(module.atomic_value, Dingus)
+
+    def should_replace_collaborating_classes(self):
+        assert isinstance(module.Collaborator, Dingus)
+
+    def should_replace_class_under_test_with_clone(self):
+        assert module.instance_under_test is not instance_under_test
+        assert not isinstance(module.instance_under_test, Dingus)
+
+    def should_leave_method_under_test_intact(self):
+        eq_(module.instance_under_test.method_under_test.im_func,
+            instance_under_test.method_under_test.im_func)
+        assert (module.instance_under_test.method_under_test()
+                == 'return value of undingused method under test')
+
+    def should_replace_other_methods(self):
+        assert isinstance(module.instance_under_test.collaborator_method, Dingus)
+
+
+class WhenCallingTeardownFunctionAndTestingMethodOfInstance:
+    def setup(self):
+        self.original_module_dict = module.__dict__.copy()
+        class TestCase(
+            DingusTestCase(
+                module.instance_under_test.method_under_test)):
+            pass
+        test_case_object = TestCase()
+        test_case_object.setup()
+        test_case_object.teardown()
+
+    def should_restore_module_attributes(self):
+        assert module.atomic_value is 'foo'
+
+    def should_leave_globals_as_they_were_before_dingusing(self):
+        assert module.__dict__ == self.original_module_dict