Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I'm unable to mock inherited methods in a subclass when the parent is also mocked #218

Open
nsheridan opened this issue Jul 17, 2020 · 1 comment
Labels
bug Something isn't working

Comments

@nsheridan
Copy link

I'm using TestSlide version: 2.5.7

Given: https://gist.github.com/nsheridan/a08dc79a84cc2974710b7a02b78ff77b

When I run:

% testslide test.py

I expected this to happen:
The test test_parent_and_child_no_override should pass

But, instead this happened:
When both the child and parent classes are mocked I get a NonExistentAttribute exception when I attempt to mock a non-overridden method in the child class:

% testslide test.py
test.TestThing
  test_child
  test_parent
  test_parent_and_child_no_override: NonExistentAttribute: 'a_method' can not be set.
  test_parent_and_child_no_override_careful_ordering
  test_parent_and_child_override

Failures:

  1) test.TestThing: test_parent_and_child_no_override
    1) NonExistentAttribute: 'a_method' can not be set.
    <StrictMock 0x10EF3CBD0 template=test.AChild test.py:32> template class does not have this attribute so the mock can not have it as well.
    See also: 'runtime_attrs' at StrictMock.__init__.
      File "/Users/nsheridan/homebrew/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/contextlib.py", line 119, in __exit__
        next(self.gen)
      File "/Users/nsheridan/homebrew/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor
        yield
      File "/Users/nsheridan/homebrew/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 628, in run
        testMethod()
      File "test.py", line 38, in test_parent_and_child_no_override
        self.mock_callable(achild, "a_method").to_return_value(None)

Finished 5 example(s) in 0.2s: .
  Successful: 4
  Failed: 1

As you can see from the example, it is possible to

  • mock a method in a child class before the parent is mocked
  • mock a method in a child class after the parent is mocked if the method is overridden in the child

However if the parent and child classes are both mocked it is not possible to mock non-overridden methods in the child class

@fornellas
Copy link
Contributor

Thanks for reporting this!

This bug is a result of the extremely hacky way that mock_constructor is implemented due to an upstream Python bug...

The gist is:

When self.mock_constructor(sys.modules[__name__], "AClass").to_return_value(aclass) is called:

  • It must empty all attributes of AClass,
  • Create a subclass of it,
  • Put all attributes back at the subclass (but mocked),
  • Finally put this subclass in place of AClass at the module.

So, when achild = testslide.StrictMock(AChild) runs, it gets a reference to the original AChild class. But when self.mock_constructor(sys.modules[__name__], "AChild").to_return_value(achild) runs, it creates the new mocked class, which should be accessed via getattr(sys.modules[__name__], "AChild") exclusively, however, achild still has the reference for the old empty class.

mock_constructor has safeguards in place against similar cases, and it'll refuse to work if there are pre-existing instances of the mocked class. But the check, is currently not covering this case and needs improvement.

I drafted a fix:

diff --git a/testslide/mock_constructor.py b/testslide/mock_constructor.py
index a85fa6a..0b12ef5 100644
--- a/testslide/mock_constructor.py
+++ b/testslide/mock_constructor.py
@@ -315,13 +315,21 @@ def mock_constructor(target, class_name, allow_private=False, type_validation=Tr
         instances = [
             obj
             for obj in gc.get_referrers(original_class)
-            if type(obj) is original_class
+            # FIXME exclude all allowed cases, eg: parent class, references at this module.
+            if id(obj) != id(target)
         ]
         if instances:
+            references_str = "> " + "\n\n > ".join(f"{id(obj)}: {type(obj)}\n  {repr(obj)}" for obj in instances)
             raise RuntimeError(
-                "mock_constructor() can not be used after instances of {} were created: {}".format(
-                    class_name, instances
-                )
+                "mock_constructor() can not be used with multiple references to the class:\n"
+                "Due to an upstream Python bug, mock_constructor() implementation contains "
+                "a lot of workarounds "
+                "(https://testslide.readthedocs.io/en/master/patching/mock_constructor/index.html#implementation-details). "
+                "As a result, it is impossible to support its usage in scenarios where:\n"
+                "- Instances of the class have already been created\n."
+                "- Multiple references to the class exist.\n"
+                f"The following live objects have references to {class_name}:\n\n"
+                + references_str
             )
 
         if not inspect.isclass(original_class):

But as the FIXME comment tells, it still needs some love, this is gonna be tricky, not only to add all exceptions, but to also, give a meaningful error. Eg: the object with the reference can be a dictionary of another class (eg: OtherClass.whatever = AChild), and just printing this dictionary at the error, won't make it obvious that it belongs to OtherClass.

@fornellas fornellas added the bug Something isn't working label Jul 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants