0

How do I get a method of a @patch'ed class to throw an Exception when called?

B is just a class that you can call go() on, which in turn just prints that it happened:

# module_b.py

class B:
  def __init__(self) -> None:
    self.x = True
    print("B constructed OK")
  def go(self):
    print("called B.go()")

A is a class that holds an instance of B. It also has a go() method, which calls the B instance's go() method:

# module_a.py

from module_b import B

class A:
  def __init__(self) -> None:
    try:
      self.b = B()
      print("A constructed OK")
    except Exception as e:
      print("A's constructor threw an exception: " + repr(e))
  def go(self):
    try:
      self.b.go()
      print("b.go() called with no problems")
    except Exception as e:
      print("a.go() threw an exception: " + repr(e))

And here's the test code:

# main.py

from module_a import A
from module_b import B
from unittest import mock

# a = A()
# a.go() # output: called B.go()

@mock.patch("module_a.B") # learned the hard way to do this, not "module_b.B"
def test_b_constructor_throws(mock_b: mock.Mock):
  mock_b.side_effect = Exception("test")
  a = A()
  
print("-- test_b_constructor_throws() --")
test_b_constructor_throws()
print("")
  
@mock.patch("module_b.B.go")
@mock.patch("module_a.B")
def test_b_method_go_throws_1(
    mock_b: mock.Mock,
    mock_b_go: mock.Mock
    ):
      
  # --- attempt 1 ---
  # mock_b_go.side_effect = Exception("test")
  # a = A()
  # a.go()
  
  # --- attempt 2 ---
  mock_b.return_value.mock_b_go.side_effect = Exception("test")
  a = A()
  a.go()
  
print("-- test_b_method_go_throws_1() --")
test_b_method_go_throws_1()
print("")

Finally, here's the output:

-- test_b_constructor_throws() --
B's constructor threw an exception: Exception('test')

-- test_b_method_go_throws_1() --
A constructed OK
b.go() called with no problems

Above code on Trinket.io

I've tried a variety of other things too, like autospec=True and using @patch.object() instead, but no go. I've read a bunch of seemingly related questions but have been unable to come up with a solution:

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • 2
    `A` is pretty badly implemented. If it fails to create an instance of the collaborator, which shouldn't be happening in `__init__` anyway (invert the dependency and inject it as an argument, then using a test double is easy without any patching), it just _prints_ that things are now totally broken? – jonrsharpe Jan 03 '23 at 17:30
  • In terms of making the current implement work note your second attempt is closer, but `mock_b.return_value` would be a `B`, which doesn't _have_ a `mock_b_go` attribute (that's the name of the other patch, which is targeting the wrong module). – jonrsharpe Jan 03 '23 at 17:34
  • (In fact note that if you `@mock.patch("module_a.B", spec=B)` unittest will tell you in no uncertain terms that `mock_b.return_value.mock_b_go.side_effect` doesn't make any sense.) – jonrsharpe Jan 03 '23 at 17:46
  • @jonrsharpe - Ah, I removed a lot of code to try to achieve the Minimal part of MVCE—the actual code doesn't just print things. The original, existing code is weirder and I don't know if I want to mess with it, _e.g._ A is a Singleton, etc. I'm trying to gain insight using a simpler example. (But I'll still think about whether it's possible to invert the dependency as you say.) Hm, I think I understand what you mean about `B` not having a `mock_b_go` attribute. I'm still not quite sure how to make this work. But I'll play around with your suggestions, thank you. – Andrew Cheong Jan 03 '23 at 17:55
  • I think I got it, thanks @jonrsharpe! I'm going to add a (self-)answer because I think this question _could_ help someone in the future wording the question or having the same misconceptions as I did. If you add an answer though I'll delete mine and accept yours of course. – Andrew Cheong Jan 03 '23 at 18:07

1 Answers1

0

@jonrsharpes' comments as well as reading his answer here got me where I needed.

  # --- attempt 1 ---
  # mock_b_go.side_effect = Exception("test")
  # a = A()
  # a.go()
  
  # --- attempt 2 ---
  # mock_b.return_value.mock_b_go.side_effect = Exception("test")
  # a = A()
  # a.go()
  
  # --- attempt 3 ---
  mock_b().go.side_effect = Exception("test")
  a = A()
  a.go()
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145