2

I'm trying to mock a library (matplotlib for what it's worth), and am hitting an issue where it fails when the mock is called expecting a tuple returned. Is there a better way to do this?

Python 3.7.2 (default, Jan 13 2019, 12:50:15) 
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin

>>> import mock
>>> foo = mock.MagicMock()
>>> a, b = foo()

This is the error message received:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 2, got 0)
>>> 
LHM
  • 721
  • 12
  • 31
TinyTheBrontosaurus
  • 4,010
  • 6
  • 21
  • 34
  • 1
    it's not a duplicate. The other one wants to change the return value between calls to the function. This one wants to return multiple MagicMocks – TinyTheBrontosaurus Apr 22 '19 at 12:07

2 Answers2

2

(For those who got here curious about one mock returning different distinct values, see my use of side_effect at the end of my answer below.)

I'll start with the first statement in your question:

I'm trying to mock a library (matplotlib for what it's worth)

If you are able to mock this library properly, you shouldn't have to worry about the behavior of any the subfunctions - as they will all be mocked and you can determine how the mocked functions should behave. How I have achieved similar behavior is by using patch - specifically, patch.object should help:

import mock

mock_matplotlib = mock.MagicMock()
mock_matplotlib.return_value = whatever_mock_behavior_you_want

with mock.patch.object(my_app, "matplotlib", mock_matplotlib):
    # Call the function you are testing which uses matplotlib

You now have the freedom to substitute whatever you'd like in for mock_matplotlib so that it behaves in the way you seek... (i.e. mock_matplotlib.return_value = mock_tuple) Let's explore some of those possibilities.

You are correct when you mentioned that MagicMock always returns a MagicMock for anything called on it - however, with that statement of a, b = foo(), Python is expecting to find a tuple and the error is triggering before the MagicMock can create the default single MagicMock return value, so the error says that it finds 0 values to work with.

Your solution of creating a tuple of MagicMocks could work a number of different ways - it is not clear from your question whether assigning values with a, b = is more important to you or not. If it is then you could do this:

import mock
foo = mock.MagicMock()
a, b = (mock.MagicMock(), mock.MagicMock())

or this:

a, b = (foo(), foo())

Or, if you are more interested in having one function call fill those values for you, as mentioned, you could assign a return_value like this:

foo = mock.MagicMock()
foo.return_value = (mock.MagicMock(), mock.MagicMock())
a, b = foo()

or even like this:

foo = mock.MagicMock(return_value=(mock.MagicMock(), mock.MagicMock()))
a, b = foo()

For those who are curious about this comment, the OP is correct that this is different from returning a different value each time the MagicMock is called. That would use the side_effect option - however, even that could be used to achieve the OP's goal - albeit in a roundabout way:

mock_1 = mock.MagicMock()
mock_2 = mock.MagicMock()
foo = mock.MagicMock(side_effect=[mock_1, mock_2])

a, b = (foo(), foo())

In that last example, mock_1 is assigned to a, and mock_2 is assigned to b.

LHM
  • 721
  • 12
  • 31
  • i guess the issue i'm hitting is that the matplotlib API is pretty complicated, so I don't want to have to write special cases every time a function might return a tuple. – TinyTheBrontosaurus May 18 '21 at 21:48
  • @TinyTheBrontosaurus - is the line `a, b = ...` in your actual code being tested? I added a bit to the front of my answer about mocking the whole `matplotlib` library. – LHM May 24 '21 at 18:33
0

You can set the return_value attribute of the MagicMock object with a tuple of your choice:

import mock
foo = mock.MagicMock()
foo.return_value = 1, 2
a, b = foo()
assert a == 1
assert b == 2
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • 1
    hmm. that's do-able but non-ideal. usually when i use `MagicMock` it always returns a `MagicMock` for anything called on it. so now what i'd do is `foo.return_value = (mock.MagicMock, mock.MagicMock)` (or my my specific case `matplotlib.plt.subplots.return_value = (mock.MagicMock, mock.MagicMock)` – TinyTheBrontosaurus Mar 15 '19 at 00:41